Le applicazioni web odierne possono diventare piuttosto grandi, in particolare la loro parte JavaScript. A partire dalla metà del 2018, HTTP Archive imposta le dimensioni mediane di trasferimento di JavaScript sui dispositivi mobili a circa 350 kB. Queste sono solo le dimensioni del trasferimento. JavaScript è spesso compresso quando viene inviato attraverso la rete, il che significa che la quantità effettiva di JavaScript è leggermente superiore dopo che il browser l'ha decompressa. È importante sottolineare che, per quanto riguarda l'elaborazione delle risorse, la compressione è irrilevante. 900 KB di JavaScript decompresso sono ancora 900 KB per l'analizzatore sintattico e il compilatore, anche se possono essere circa 300 KB quando vengono compressi.
JavaScript è una risorsa costosa da elaborare. A differenza delle immagini che prevedono tempi di decodifica relativamente banali una volta scaricate, JavaScript deve essere analizzato, compilato ed eseguito. Byte per byte, questo rende JavaScript più costoso rispetto ad altri tipi di risorse.
Mentre vengono apportati continui miglioramenti per migliorare l'efficienza dei motori JavaScript, il miglioramento delle prestazioni di JavaScript è, come sempre, un compito degli sviluppatori.
A questo scopo, esistono tecniche per migliorare le prestazioni di JavaScript. La suddivisione del codice è una tecnica di questo tipo che migliora le prestazioni partizionando il codice JavaScript dell'applicazione in blocchi e pubblicando questi blocchi solo nelle route di un'applicazione che ne ha bisogno.
Sebbene questa tecnica funzioni, non risolve un problema comune delle applicazioni con uso intensivo di JavaScript, ovvero l'inclusione di codice mai utilizzato. La scuotimento degli alberi tenta di risolvere questo problema.
Che cosa sono le scosse degli alberi?
Lo scuotimento degli alberi è una forma di eliminazione dei codici morti. Il termine è stato reso popolare da Rollup, ma il concetto di eliminazione di codice non valido esiste da un po' di tempo. Il concetto ha rilevato anche l'acquisto in webpack, come dimostrato in questo articolo tramite un'app di esempio.
Il termine "trees shaking" deriva dal modello mentale dell'applicazione e dalle sue dipendenze in una struttura ad albero. Ogni nodo nella struttura ad albero rappresenta una dipendenza che fornisce funzionalità distinte per la tua app. Nelle app moderne, queste dipendenze vengono inserite tramite istruzioni import
statiche, ad esempio:
// Import all the array utilities!
import arrayUtils from "array-utils";
Quando un'app è giovane (un alberello, se vuoi), potrebbe avere poche dipendenze. Sta anche utilizzando la maggior parte delle dipendenze, se non tutte. Man mano che la tua app matura, tuttavia, possono essere aggiunte altre dipendenze. Per aggravare le questioni, le dipendenze meno recenti non sono più in uso, ma potrebbero non essere eliminate dal codebase. Il risultato finale è che un'app viene spedito con una grande quantità di JavaScript non utilizzato. La scossa ad albero consente di risolvere questo problema sfruttando il modo in cui le istruzioni import
statiche richiamano parti specifiche dei moduli ES6:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
La differenza tra questo esempio import
e quello precedente è che, anziché importare tutto dal modulo "array-utils"
, che potrebbe richiedere una quantità di codice elevato, questo esempio ne importa solo parti specifiche. Nelle build dev non cambia nulla, dato che in ogni caso viene importato l'intero modulo. Nelle build di produzione, il webpack può essere configurato in modo da "scuotere" le esportazioni dai moduli ES6 che non sono stati importati esplicitamente, riducendo così le dimensioni delle build di produzione. In questa guida, imparerai come fare.
Trovare opportunità per scuotere un albero
A scopo illustrativo, è disponibile un'app di esempio di una pagina che dimostra come funziona la scossa degli alberi. Puoi clonarlo e seguirlo se vuoi, ma in questa guida tratteremo tutti i passaggi del percorso, quindi la clonazione non è necessaria (a meno che tu non voglia imparare a fare pratica).
L'app di esempio è un database disponibile per la ricerca di pedali per effetti per chitarra. Se inserisci una query, viene visualizzato un elenco di pedali degli effetti.
Il comportamento alla base di questa app è separato in base al fornitore (ad esempio, Preact ed Emotion) e bundle di codice specifici dell'app (o "blocchi", come li chiama il webpack):
I bundle JavaScript mostrati nella figura sopra sono build di produzione, ovvero sono ottimizzate attraverso l'uglificazione. 21,1 kB per un bundle specifico dell'app non sono male, ma va tenuto presente che non si verifica alcuna scossa degli alberi. Diamo un'occhiata al codice dell'app e vediamo cosa si può fare per risolvere il problema.
In qualsiasi applicazione, trovare opportunità di scosseare gli alberi implica la ricerca di affermazioni import
statiche. Nella parte superiore del file del componente principale, vedrai una riga simile alla seguente:
import * as utils from "../../utils/utils";
Puoi importare i moduli ES6 in diversi modi, ma quelli come questo dovrebbero attirare la tua attenzione. Questa riga specifica indica "import
tutto del modulo utils
e lo mette in uno spazio dei nomi denominato utils
." La domanda principale è "quanto ci sono contenuti in quel modulo?"
Se osservi il codice sorgente del modulo utils
, vedrai che ci sono circa 1300 righe di codice.
Ti ti serve tutta quella roba? Controlliamo attentamente il numero di istanze di quello spazio dei nomi nel file del componente principale che importa il modulo utils
.
Lo spazio dei nomi utils
appare solo in tre punti della nostra applicazione, ma per quali funzioni? Se esamini di nuovo il file del componente principale, ti sembra che sia presente una sola funzione, ovvero utils.simpleSort
, utilizzata per ordinare l'elenco dei risultati di ricerca in base a una serie di criteri quando vengono modificati i menu a discesa di ordinamento:
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
Su un file di 1300 righe con un gruppo di esportazioni, ne viene utilizzato solo uno. Ciò si traduce nella spedizione di una grande quantità di codice JavaScript inutilizzato.
Anche se questa app di esempio è certamente un po' inventata, non cambia il fatto che questo tipo di scenario sintetico assomiglia a opportunità di ottimizzazione reali che potresti trovare in un'app web di produzione. Dopo aver identificato un'opportunità che l'agitazione degli alberi sia utile, come viene effettivamente fatto?
Impedire a Babel di transpirare i moduli ES6 in moduli CommonJS
Babel è uno strumento indispensabile, ma può rendere un po' più difficile osservare gli effetti del tremolio degli alberi. Se utilizzi @babel/preset-env
, Babel potrebbe trasformare i moduli ES6 in moduli CommonJS più compatibili, ovvero moduli require
anziché import
.
Poiché l'oscillazione degli alberi è più difficile da eseguire per i moduli CommonJS, webpack non saprà cosa eliminare dai bundle se decidi di usarli. La soluzione consiste nel configurare @babel/preset-env
in modo da lasciare esplicitamente i moduli ES6. Ovunque configuri Babel, in babel.config.js
o package.json
, dovrai aggiungere qualcosa in più:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Se specifichi modules: false
nella configurazione @babel/preset-env
, Babel si comporti come previsto, consentendo al webpack di analizzare la tua struttura ad albero delle dipendenze e di eliminare le dipendenze inutilizzate.
Aspetti da considerare negli effetti collaterali
Un altro aspetto da considerare quando si sconvolgono le dipendenze dall'app è se i moduli del progetto hanno effetti collaterali. Un esempio di effetto collaterale è quando una funzione modifica qualcosa al di fuori del suo ambito, che è un effetto collaterale della sua esecuzione:
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
In questo esempio, addFruit
produce un effetto collaterale quando modifica l'array fruits
, che esula dal suo ambito.
Gli effetti collaterali si applicano anche ai moduli ES6, e ciò è importante nel contesto dell'agitazione degli alberi. I moduli che accettano input prevedibili e producono output altrettanto prevedibili senza modificare nulla al di fuori del proprio ambito sono dipendenze che possono essere eliminate in sicurezza se non vengono utilizzate. Sono parti di codice modulari indipendenti. Quindi, "moduli".
Per quanto riguarda il webpack, è possibile usare un suggerimento per specificare che un pacchetto e le sue dipendenze sono privi di effetti collaterali specificando "sideEffects": false
nel file package.json
di un progetto:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
In alternativa, puoi indicare al webpack quali file specifici non sono privi di effetti collaterali:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
Nel secondo esempio, qualsiasi file non specificato verrà considerato privo di effetti collaterali. Se non vuoi aggiungerlo al file package.json
, puoi specificare questo flag anche nella configurazione del webpack tramite module.rules
.
Importazione solo degli elementi necessari
Dopo aver indicato a Babel di non modificare i moduli ES6, è necessario apportare una leggera modifica alla sintassi import
per inserire solo le funzioni necessarie dal modulo utils
. Nell'esempio di questa guida, è sufficiente la funzione simpleSort
:
import { simpleSort } from "../../utils/utils";
Poiché viene importato solo simpleSort
anziché l'intero modulo utils
, ogni istanza di utils.simpleSort
dovrà essere modificata in simpleSort
:
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
Questo dovrebbe essere tutto ciò che serve affinché la scossa degli alberi funzioni in questo esempio. Questo è l'output del webpack prima di scuotere l'albero delle dipendenze:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
Questo è l'output dopo che la scossa dell'albero ha avuto esito positivo:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
Sebbene entrambi i bundle si siano ridotti, è davvero il bundle main
che ne trae i maggiori vantaggi. Eliminando le parti inutilizzate del modulo utils
, il bundle main
si riduce di circa il 60%. In questo modo, non solo riduci il tempo impiegato dallo script per il download, ma anche il tempo di elaborazione.
Scuoti qualche albero!
La distanza percorsa dalle scosse degli alberi dipende dalla tua app, dalle sue dipendenze e dall'architettura. Prova. Se sai per certo di non aver configurato il bundler di moduli per eseguire questa ottimizzazione, provare e vedere i vantaggi per la tua applicazione non comporta alcun danno.
Puoi ottenere un miglioramento del rendimento significativo in seguito alle scosse degli alberi, o addirittura ottenere risultati parziali. Tuttavia, configurando il sistema di compilazione per sfruttare questa ottimizzazione nelle build di produzione e importando selettivamente solo ciò di cui la tua applicazione ha bisogno, eviterai proattivamente di ridurre al minimo le dimensioni dei bundle di applicazioni.
Un ringraziamento speciale a Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone e Philip Walton per il loro prezioso feedback, che ha notevolmente migliorato la qualità di questo articolo.