Il JavaScript è la risorsa più costosa di una pagina, byte per byte. Un'immagine da 200 KB va solo decodificata e disegnata; uno script della stessa dimensione va scaricato, analizzato, compilato ed eseguito sul main thread, e su un telefono di fascia media questo lavoro può richiedere molto più tempo che su un portatile. Ridurre la quantità di JavaScript inviato al primo caricamento è quindi uno degli interventi più efficaci, e i due strumenti principali sono il code splitting e il tree shaking.
Il problema del bundle unico
Molti progetti nascono con un unico file JavaScript che contiene tutto: il codice della home, quello del carrello, l'editor dell'area riservata, la libreria dei grafici usata in una sola pagina. Ogni visitatore scarica ed esegue l'intero pacchetto, anche se gli serve un decimo del codice.
Il code splitting divide questo file in più parti, caricate solo quando servono. Il tree shaking, invece, elimina dal risultato finale il codice importato ma mai usato.
Code splitting con l'import dinamico
La base del code splitting è l'import() dinamico, che restituisce una promessa e indica allo strumento di build di creare un file separato:
pulsanteGrafico.addEventListener('click', async () => {
const { disegnaGrafico } = await import('./grafici.js');
disegnaGrafico(contenitore, dati);
});
La libreria dei grafici non entra più nel bundle principale: viene scaricata solo da chi preme il pulsante. Gli strumenti di build moderni, come webpack, Rollup, Vite ed esbuild, riconoscono questa sintassi e generano automaticamente i file separati.
Dove dividere
I punti di divisione più redditizi sono:
- per pagina o percorso: in un'applicazione con più sezioni, ogni percorso carica solo il proprio codice. Quasi tutti i framework lo supportano in modo nativo;
- per componente pesante: editor di testo ricco, mappe, grafici, lettori video, finestre di dialogo complesse;
- per funzionalità rara: esportazione in PDF, strumenti di amministrazione, procedure di configurazione iniziale;
- per condizione: polyfill caricati solo nei browser che ne hanno bisogno.
Attenzione all'eccesso opposto: dividere in decine di file minuscoli moltiplica le richieste e può creare catene in cui un pezzo scopre il successivo solo dopo essere stato eseguito. Con HTTP/2 molte richieste parallele non sono un problema, ma le catene sì. Per i pezzi che serviranno quasi certamente, modulepreload o un precaricamento al passaggio del mouse riducono l'attesa.
Tree shaking: eliminare il codice morto
Il tree shaking analizza gli import e gli export dei moduli ES e scarta le funzioni che nessuno usa. Perché funzioni servono alcune condizioni:
- moduli ES: con la sintassi
import/exportlo strumento di build può analizzare le dipendenze in modo statico. Con CommonJS (require) l'analisi è molto più limitata; - import mirati:
import { debounce } from 'libreria'permette di includere solo quella funzione, mentre importare l'intero oggetto della libreria rende difficile scartare il resto; - assenza di effetti collaterali: se un modulo, al solo caricamento, modifica oggetti globali, il bundler non può eliminarlo con sicurezza. Il campo
"sideEffects": falsenelpackage.jsondi una libreria dichiara che i suoi moduli si possono scartare se non usati.
Un esempio classico: alcune librerie di utilità esistono in due versioni, una CommonJS e una a moduli ES. Passare alla seconda e importare singole funzioni può ridurre drasticamente la quota di quella dipendenza nel bundle.
Scoprire cosa pesa
Prima di ottimizzare bisogna sapere dove guardare. Gli strumenti utili sono:
| Strumento | Cosa mostra |
|---|---|
| Analizzatori di bundle (per webpack, Rollup, Vite) | Mappa visiva delle dipendenze e del loro peso |
| Pannello Coverage di DevTools | Percentuale di codice scaricato ma non eseguito |
| Report di build | Dimensione di ogni file prodotto, compressa e non |
| Siti di analisi dei pacchetti npm | Peso di una libreria prima di installarla |
La mappa di un analizzatore rivela spesso sorprese ricorrenti: una libreria per le date che include tutte le lingue del mondo, due versioni dello stesso pacchetto installate da dipendenze diverse, un'intera libreria di icone per usarne tre, polyfill per browser che il sito non deve più supportare.
Interventi tipici
Nel mio lavoro, i guadagni più consistenti arrivano quasi sempre da pochi interventi mirati:
- sostituire dipendenze voluminose con alternative più leggere o con funzioni native del browser, come
Intlper formattare date e numeri; - caricare in modo dinamico i componenti presenti solo in alcune pagine;
- impostare il target di compilazione sui browser effettivamente supportati, evitando trasformazioni e polyfill superflui;
- eliminare le dipendenze duplicate allineando le versioni.
Tenere i risultati nel tempo
Il bundle tende a ricrescere a ogni nuova funzionalità. Conviene fissare un limite di dimensione per i file principali e farlo verificare a ogni modifica: strumenti come size-limit falliscono la build se il limite viene superato, trasformando una regressione silenziosa in un problema visibile da discutere prima della pubblicazione.
In sintesi
- Il JavaScript costa più di altre risorse perché va analizzato ed eseguito.
- Il code splitting con
import()carica il codice solo quando serve. - Il tree shaking richiede moduli ES, import mirati e librerie senza effetti collaterali.
- Analizzatori di bundle e pannello Coverage mostrano dove intervenire.
- Fissa limiti di dimensione verificati automaticamente per evitare regressioni.