⚡ Prestazioni web

Code splitting e tree shaking: ridurre il bundle JavaScript

Come dividere il JavaScript in pezzi caricati quando servono, eliminare il codice morto dalle dipendenze e scoprire cosa gonfia davvero il bundle.

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:

  1. 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;
  2. per componente pesante: editor di testo ricco, mappe, grafici, lettori video, finestre di dialogo complesse;
  3. per funzionalità rara: esportazione in PDF, strumenti di amministrazione, procedure di configurazione iniziale;
  4. 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/export lo 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": false nel package.json di 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 Intl per 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.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche