JavaScript moderno per siti veloci: import dinamici, code splitting e tree shaking spiegati senza gergo

Pacchetti, divisione del codice, importazioni dinamiche e tree shaking spiegati in modo semplice: come si riduce il JavaScript di un sito, come vedere cosa c’è nel pacchetto e cosa chiedere a chi sviluppa.

Un grande blocco JavaScript giallo diviso in un pezzo di base verde e altri pezzi tratteggiati caricati solo quando servono, con un’importazione dinamica

Tra tutte le risorse di una pagina, il JavaScript è la più costosa. Un’immagine da 200 kilobyte, una volta scaricata, va solo decodificata e disegnata. Uno script da 200 kilobyte va scaricato, poi analizzato, compilato ed eseguito sul main thread, lo stesso che deve rispondere ai tocchi e ai clic dell’utente. Su uno smartphone di fascia media questo lavoro può richiedere centinaia di millisecondi, con effetti diretti sull’INP e, spesso, anche sull’LCP.

Gli strumenti con cui oggi si costruisce il JavaScript di un sito offrono tecniche efficaci per ridurne il costo: dividere il codice in pezzi caricati solo quando servono, eliminare automaticamente le parti non usate, evitare di tradurre il codice per browser che nessuno usa più. Sono concetti spesso avvolti in un gergo tecnico, bundle, chunk, tree shaking, che scoraggia chi non scrive codice tutti i giorni. In questo articolo li spieghiamo in modo semplice, con esempi concreti, per chi deve capire cosa chiedere a uno sviluppatore o valutare il lavoro di un fornitore.

Dal codice sorgente al pacchetto

Il JavaScript di un sito moderno non viene scritto come un unico file. È diviso in moduli: uno per il menu, uno per la galleria, uno per il carrello, più le librerie di terze parti su cui si appoggia. Prima di essere pubblicato, uno strumento chiamato bundler, come Vite, webpack, esbuild o Rollup, raccoglie tutti questi moduli, risolve le dipendenze tra di loro e produce uno o più file ottimizzati per il browser, detti pacchetti o bundle. Durante questo passaggio può anche minificare il codice, cioè togliere spazi e commenti e accorciare i nomi, e adattarlo ai browser a cui è destinato.

La configurazione più semplice produce un unico pacchetto con tutto il codice del sito, caricato su ogni pagina. È comoda, ma ogni pagina paga il costo di tutte le funzioni, comprese quelle che non usa: la home scarica il codice del checkout, l’articolo del blog quello della galleria prodotti. Man mano che il sito cresce, il pacchetto cresce con lui.

Code splitting: caricare solo ciò che serve

Il code splitting, la divisione del codice, risolve il problema producendo più pacchetti invece di uno. Un pacchetto di base contiene ciò che serve ovunque, come il menu e le funzioni comuni; altri pacchetti, detti chunk, contengono le funzioni specifiche e vengono caricati solo nelle pagine o nei momenti in cui servono.

Confronto tra un pacchetto unico da 600 kilobyte caricato su ogni pagina, con menu, galleria, checkout, ricerca, editor delle recensioni, mappa, chat e animazioni, e pezzi separati: una base da 80 kilobyte sempre caricata, la galleria da 60 nella scheda prodotto, il checkout da 90 solo nel checkout, la ricerca da 40 al primo tocco sulla lente, l’editor da 200 solo se si scrive una recensione
Con la divisione del codice ogni pagina scarica solo la base e i pezzi che usa davvero.

La divisione può seguire due criteri. Per pagina, o per percorso: ogni tipo di pagina ha il proprio pacchetto, e la scheda prodotto non carica il codice del checkout. Per funzione: le parti che servono solo dopo un’azione dell’utente, come l’editor per scrivere una recensione, il visualizzatore delle immagini a schermo intero o la ricerca con suggerimenti, vengono caricate solo quando l’utente le usa. Lo strumento per farlo è l’importazione dinamica, una funzione del linguaggio che carica un modulo al momento del bisogno:

const pulsante = document.querySelector( '.apri-galleria' );

pulsante.addEventListener( 'click', async () => {
	// Il codice della galleria viene scaricato solo al primo clic
	const { apriGalleria } = await import( './galleria.js' );
	apriGalleria( pulsante.dataset.prodotto );
} );

Il bundler riconosce import() e produce automaticamente un pacchetto separato per galleria.js e le sue dipendenze. Il primo clic richiede un breve download, che può essere anticipato quando l’utente si avvicina al pulsante o con un modulepreload, come spiegato nell’articolo su preload, preconnect e fetchpriority; tutti gli utenti che non aprono la galleria, invece, non pagano nulla.

La divisione va fatta con misura. Troppi pezzi piccoli moltiplicano le richieste e possono creare catene, in cui un pezzo scopre il successivo solo dopo essere stato eseguito. Una buona regola è separare ciò che è grande e usato da pochi, e lasciare insieme ciò che è piccolo e usato da molti.

Tree shaking: eliminare ciò che non si usa

Il secondo concetto riguarda le librerie. Una libreria di utilità può offrire decine di funzioni, e un sito ne usa magari tre. Se il codice importa l’intera libreria, il pacchetto la contiene tutta. Il tree shaking, letteralmente «scuotere l’albero», è la capacità del bundler di analizzare quali funzioni vengono davvero importate e usate e di eliminare tutte le altre, come foglie secche che cadono.

Illustrazione del tree shaking: un albero con 24 foglie che rappresentano le funzioni di una libreria, di cui solo tre usate e evidenziate in verde; l’importazione dell’intera libreria pesa 300 kilobyte, l’importazione delle sole funzioni formatta, arrotonda e ordina pesa 12 kilobyte perché il bundler elimina ciò che non è importato
Il bundler conserva solo le funzioni importate e usate.

Perché funzioni servono due condizioni. La prima è che il codice usi i moduli standard di JavaScript, con istruzioni import ed export che il bundler può analizzare prima dell’esecuzione; le librerie scritte con i sistemi di moduli più vecchi non si prestano a questa analisi. La seconda è che la libreria dichiari di non avere effetti collaterali, cioè che importarne un pezzo non esegua codice necessario altrove. Le librerie moderne sono in genere pensate per questo; per quelle più vecchie esistono spesso versioni alternative o pacchetti separati per singola funzione.

// Importa l’intera libreria: il pacchetto la contiene tutta
import _ from 'lodash';
_.debounce( aggiorna, 200 );

// Importa solo la funzione usata: il resto viene eliminato
import { debounce } from 'lodash-es';
debounce( aggiorna, 200 );

Un’insidia frequente sono i file che riesportano tutto da un unico punto, comodi per chi scrive il codice ma capaci, in alcune configurazioni, di trascinare nel pacchetto molto più del necessario. Il modo per scoprirlo è guardare cosa finisce davvero nel pacchetto, con gli strumenti di analisi descritti più avanti.

Scrivere per i browser di oggi

Per anni il JavaScript è stato tradotto, durante la compilazione, in una versione compatibile con browser molto vecchi, e accompagnato da polyfill, cioè codice che riproduce funzioni mancanti. Il risultato era un codice più voluminoso e più lento da eseguire anche per chi usava un browser moderno. Oggi i browser in uso supportano quasi tutte le funzioni moderne del linguaggio, e configurare il bundler per i browser realmente usati dai visitatori, invece che per quelli di dieci anni fa, riduce il peso del pacchetto senza alcun effetto per gli utenti. I dati degli analytics sui browser dei visitatori sono la base per questa scelta.

Le dipendenze: il peso che non si vede

In molti progetti la parte più grande del JavaScript non è il codice scritto dagli sviluppatori, ma quello delle librerie su cui si appoggia. Una libreria per le date, una per le animazioni, una per i caroselli, un framework per l’interfaccia: ciascuna aggiunge decine o centinaia di kilobyte. Prima di aggiungere una dipendenza conviene chiedersi se il browser non offra già la funzione: la formattazione di date e numeri con le API di internazionalizzazione, le animazioni con il CSS, la selezione degli elementi e le richieste di rete con le funzioni native, che hanno reso superflua gran parte di jQuery. Servizi online permettono di conoscere in anticipo il peso di una libreria e delle sue alternative.

Come vedere cosa c’è nel pacchetto

Il primo passo di qualsiasi ottimizzazione è sapere cosa si sta caricando. Gli strumenti di analisi dei pacchetti, disponibili per tutti i principali bundler, producono una mappa visiva in cui ogni modulo è un rettangolo proporzionale al suo peso: a colpo d’occhio si vede quale libreria occupa metà del pacchetto, quale modulo compare due volte, quale codice finisce su pagine che non lo usano.

Nel browser, il pannello Coverage dei DevTools di Chrome mostra per ogni file JavaScript quanta parte è stata effettivamente eseguita durante il caricamento e l’uso della pagina. Una percentuale alta di codice inutilizzato non significa che vada eliminato, perché parte serve in momenti successivi, ma indica dove la divisione del codice potrebbe essere più efficace. Lighthouse riporta un’indicazione simile con il suggerimento di ridurre il JavaScript inutilizzato.

Il caso WordPress

In WordPress il JavaScript arriva da più fonti: il tema, i plugin, il core. Gli strumenti ufficiali di sviluppo, come il pacchetto di script per i blocchi, usano un bundler moderno e producono pacchetti separati per ogni blocco. Dalla versione 6.5 WordPress supporta anche i moduli JavaScript nativi, con una propria interfaccia per registrarli e caricarli, e i blocchi possono dichiarare script da caricare sul front-end solo quando il blocco è presente nella pagina, come abbiamo visto parlando dei temi a blocchi.

Per il codice del tema e dei plugin personalizzati, la regola più semplice è caricare ogni script solo dove serve, con condizioni nella registrazione degli script, invece di includerlo su tutte le pagine. Per i plugin di terze parti, il controllo è più limitato, ma gli strumenti di gestione degli script permettono di disattivarli nelle pagine in cui non vengono usati. E per gli script che non servono al primo caricamento, il ritardo fino alla prima interazione, descritto nell’articolo sul delay del JavaScript, è l’equivalente di un’importazione dinamica applicata dall’esterno.

// Il carosello del tema solo nelle pagine che lo contengono
add_action(
	'wp_enqueue_scripts',
	function () {
		if ( is_singular() && has_block( 'tema/carosello' ) ) {
			wp_enqueue_script_module( 'tema-carosello', get_theme_file_uri( 'js/carosello.js' ) );
		}
	}
);

Un esempio

Un negozio online con un tema sviluppato su misura carica su ogni pagina un unico file JavaScript da 540 kilobyte compressi. L’analisi del pacchetto mostra che un terzo è occupato da una libreria per le date, usata solo per mostrare i tempi di consegna, e da una libreria di animazioni completa, usata per due effetti nella home; un altro terzo è il codice del configuratore di prodotto e del checkout, presente anche negli articoli del blog. Gli interventi sono quattro: le date vengono formattate con le API native del browser, le due animazioni vengono riscritte in CSS, il configuratore e il checkout diventano pacchetti separati caricati solo nelle rispettive pagine, e il visualizzatore delle immagini a schermo intero viene caricato con un’importazione dinamica al primo tocco su una foto. Il pacchetto di base scende a 110 kilobyte; la scheda prodotto carica in totale 220 kilobyte invece di 540. Il tempo di esecuzione degli script al caricamento si riduce di oltre la metà sugli smartphone di fascia media, e l’INP delle schede prodotto nei dati di campo passa nello stato buono.

Cosa chiedere a chi sviluppa

Per chi non scrive codice ma deve valutare un progetto, alcune domande aiutano a capire se il JavaScript è gestito con attenzione. Quanto JavaScript, compresso, carica ciascun modello di pagina? Il codice è diviso per pagina e per funzione, o c’è un pacchetto unico? Quali sono le tre dipendenze più pesanti, e sono davvero necessarie? Per quali browser viene compilato il codice? Esiste un budget di prestazioni sul peso del JavaScript, controllato a ogni rilascio? Le risposte dicono molto più di un punteggio.

In sintesi

Il JavaScript è la risorsa più costosa di una pagina, perché va eseguito sullo stesso thread che risponde all’utente. I bundler moderni offrono tre leve principali per ridurne il costo: la divisione del codice, che carica ogni pezzo solo nelle pagine e nei momenti in cui serve, con le importazioni dinamiche; il tree shaking, che elimina dalle librerie le funzioni non usate; la compilazione per i browser di oggi, che evita traduzioni e polyfill superflui.

A queste si aggiungono la scelta attenta delle dipendenze, l’analisi periodica del contenuto dei pacchetti e, in WordPress, il caricamento degli script solo dove servono. Non servono architetture complesse: serve sapere cosa si sta caricando e perché, e caricare solo quello. La compressione, vista nell’articolo su Brotli, Gzip e Zstandard, riduce i byte trasferiti; ridurre il codice riduce anche il lavoro, ed è lì che si gioca l’INP.

IL PROSSIMO PASSO

Partiamo
dal tuo sito.

Raccontaci il progetto, i problemi che riscontri e i tuoi obiettivi. Definiremo insieme un intervento su misura.

Richiedi un’analisi

Ti porta al modulo della pagina Contatti.

MANAGED SERVER S.R.L.info@corewebvitals.it +39 02 5656 9681

Lunedì – venerdì · 09:30 – 19:30

Preferisci un modulo online?
Contattaci su managedserver.it ↗