JavaScript con suddivisione del codice

Il caricamento di risorse JavaScript di grandi dimensioni influisce in modo significativo sulla velocità della pagina. Dividere il codice JavaScript in blocchi più piccoli e scaricare solo ciò che è necessario per il funzionamento di una pagina durante l'avvio può migliorare notevolmente la reattività al caricamento della pagina, il che a sua volta può migliorare l'interazione con il successivo paint (INP) della pagina.

Quando una pagina scarica, analizza e compila file JavaScript di grandi dimensioni, può diventare non reattiva per periodi di tempo. Gli elementi della pagina sono visibili, in quanto fanno parte dell'HTML iniziale di una pagina e sono stilizzati tramite CSS. Tuttavia, poiché il codice JavaScript necessario per alimentare questi elementi interattivi, nonché altri script caricati dalla pagina, potrebbe analizzare ed eseguire il codice JavaScript per farli funzionare. Il risultato è che l'utente potrebbe avere l'impressione che l'interazione sia stata notevolmente ritardata o addirittura interrotta.

Spesso questo accade perché il thread principale è bloccato, in quanto JavaScript viene analizzato e compilato sul thread principale. Se questo processo richiede troppo tempo, gli elementi interattivi della pagina potrebbero non rispondere abbastanza rapidamente all'input dell'utente. Un rimedio è caricare solo il codice JavaScript necessario per il funzionamento della pagina, mentre il resto viene caricato in un secondo momento tramite una tecnica nota come suddivisione del codice. Questo modulo si concentra sulla seconda di queste due tecniche.

Riduci l'analisi e l'esecuzione di JavaScript durante l'avvio tramite la suddivisione del codice

Lighthouse genera un avviso quando l'esecuzione di JavaScript richiede più di 2 secondi e non riesce quando richiede più di 3, 5 secondi. L'analisi e l'esecuzione eccessive di JavaScript sono un potenziale problema in qualsiasi punto del ciclo di vita della pagina, in quanto possono aumentare il ritardo di input se il momento in cui l'utente interagisce con la pagina coincide con il momento in cui vengono eseguiti i task del thread principale responsabili dell'elaborazione e dell'esecuzione di JavaScript.

Inoltre, l'esecuzione e l'analisi eccessive di JavaScript sono particolarmente problematiche durante il caricamento iniziale della pagina, in quanto è il momento del ciclo di vita della pagina in cui gli utenti hanno maggiori probabilità di interagire con la pagina. Infatti, il Total Blocking Time (TBT), una metrica di reattività al caricamento, è altamente correlato all'INP, il che suggerisce che gli utenti tendono a tentare interazioni durante il caricamento iniziale della pagina.

L'audit Lighthouse che indica il tempo impiegato per l'esecuzione di ogni file JavaScript richiesto dalla pagina è utile perché può aiutarti a identificare esattamente quali script potrebbero essere candidati per la divisione del codice. Puoi quindi andare oltre utilizzando lo strumento di copertura in Chrome DevTools per identificare esattamente quali parti del JavaScript di una pagina non vengono utilizzate durante il caricamento della pagina.

La suddivisione del codice è una tecnica utile che può ridurre i payload JavaScript iniziali di una pagina. Consente di dividere un bundle JavaScript in due parti:

  • Il JavaScript necessario al caricamento della pagina e pertanto non può essere caricato in nessun altro momento.
  • Il JavaScript rimanente che può essere caricato in un secondo momento, in genere nel momento in cui l'utente interagisce con un determinato elemento interattivo della pagina.

La suddivisione del codice può essere eseguita utilizzando la sintassi dinamica import(). Questa sintassi, a differenza degli elementi <script> che richiedono una determinata risorsa JavaScript durante l'avvio, effettua una richiesta di una risorsa JavaScript in un secondo momento durante il ciclo di vita della pagina.

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

Nello snippet JavaScript precedente, il modulo validate-form.mjs viene scaricato, analizzato ed eseguito solo quando un utente sposta il cursore da uno dei campi <input> di un modulo. In questa situazione, la risorsa JavaScript responsabile della logica di convalida del modulo viene coinvolta nella pagina solo quando è più probabile che venga effettivamente utilizzata.

I bundler JavaScript come webpack, Parcel, Rollup ed esbuild possono essere configurati per dividere i bundle JavaScript in blocchi più piccoli ogni volta che rilevano una chiamata import() dinamica nel codice sorgente. La maggior parte di questi strumenti lo fa automaticamente, ma esbuild in particolare richiede di attivare questa ottimizzazione.

Note utili sulla suddivisione del codice

Sebbene la suddivisione del codice sia un metodo efficace per ridurre la contesa del thread principale durante il caricamento iniziale della pagina, è utile tenere a mente alcune cose se decidi di controllare il codice sorgente JavaScript per individuare opportunità di suddivisione del codice.

Se possibile, utilizza un bundler

È prassi comune per gli sviluppatori utilizzare i moduli JavaScript durante il processo di sviluppo. Si tratta di un ottimo miglioramento dell'esperienza di sviluppo che migliora la leggibilità e la manutenibilità del codice. Tuttavia, esistono alcune caratteristiche di prestazioni non ottimali che possono verificarsi quando i moduli JavaScript vengono spediti in produzione.

Ancora più importante, devi utilizzare un bundler per elaborare e ottimizzare il codice sorgente, inclusi i moduli che intendi suddividere. I bundler sono molto efficaci non solo nell'applicare ottimizzazioni al codice sorgente JavaScript, ma anche nel bilanciare considerazioni sulle prestazioni come le dimensioni del bundle rispetto al rapporto di compressione. L'efficacia della compressione aumenta con le dimensioni del bundle, ma i bundler cercano anche di garantire che i bundle non siano così grandi da comportare attività lunghe a causa della valutazione dello script.

Inoltre, i bundler evitano il problema di spedire un numero elevato di moduli non raggruppati tramite la rete. Le architetture che utilizzano moduli JavaScript tendono ad avere alberi di moduli grandi e complessi. Quando gli alberi dei moduli vengono separati, ogni modulo rappresenta una richiesta HTTP separata e l'interattività nella tua app web potrebbe essere ritardata se non raggruppi i moduli. Sebbene sia possibile utilizzare il suggerimento per le risorse <link rel="modulepreload"> per caricare alberi di moduli di grandi dimensioni il prima possibile, i bundle JavaScript sono comunque preferibili dal punto di vista delle prestazioni di caricamento.

Non disattivare inavvertitamente la compilazione dello streaming

Il motore JavaScript V8 di Chromium offre una serie di ottimizzazioni predefinite per garantire che il codice JavaScript di produzione venga caricato nel modo più efficiente possibile. Una di queste ottimizzazioni è nota come compilazione dello streaming che, come l'analisi incrementale dell'HTML trasmesso in streaming al browser, compila i blocchi di JavaScript trasmessi in streaming man mano che arrivano dalla rete.

Esistono due modi per assicurarti che la compilazione dello streaming avvenga per la tua applicazione web in Chromium:

  • Trasforma il codice di produzione per evitare di utilizzare i moduli JavaScript. I bundler possono trasformare il codice sorgente JavaScript in base a una destinazione di compilazione e la destinazione è spesso specifica per un determinato ambiente. V8 applicherà la compilazione in streaming a qualsiasi codice JavaScript che non utilizza moduli e puoi configurare il bundler per trasformare il codice del modulo JavaScript in una sintassi che non utilizza i moduli JavaScript e le relative funzionalità.
  • Se vuoi spedire moduli JavaScript in produzione, utilizza l'estensione .mjs. Indipendentemente dal fatto che il tuo JavaScript di produzione utilizzi moduli, non esiste un tipo di contenuto speciale per JavaScript che utilizza moduli rispetto a JavaScript che non li utilizza. Per quanto riguarda V8, disattivi la compilazione in streaming quando distribuisci moduli JavaScript in produzione utilizzando l'estensione .js. Se utilizzi l'estensione .mjs per i moduli JavaScript, V8 può assicurarsi che la compilazione in streaming per il codice JavaScript basato su moduli non venga interrotta.

Non lasciare che queste considerazioni ti dissuadano dall'utilizzare la suddivisione del codice. La suddivisione del codice è un modo efficace per ridurre i payload JavaScript iniziali per gli utenti, ma utilizzando un bundler e sapendo come preservare il comportamento di compilazione in streaming di V8, puoi assicurarti che il codice JavaScript di produzione sia il più veloce possibile per gli utenti.

Demo dell'importazione dinamica

webpack

webpack viene fornito con un plug-in denominato SplitChunksPlugin, che ti consente di configurare il modo in cui il bundler suddivide i file JavaScript. webpack riconosce sia le istruzioni dinamiche import() sia quelle statiche import. Il comportamento di SplitChunksPlugin può essere modificato specificando l'opzione chunks nella sua configurazione:

  • chunks: async è il valore predefinito e si riferisce alle chiamate dinamiche import().
  • chunks: initial si riferisce alle chiamate statiche import.
  • chunks: all copre sia le importazioni dinamiche import() che quelle statiche, consentendoti di condividere i blocchi tra le importazioni async e initial.

Per impostazione predefinita, ogni volta che webpack rileva un'istruzione import() dinamica, crea un chunk separato per quel modulo:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

La configurazione webpack predefinita per lo snippet di codice precedente genera due chunk separati:

  • Il blocco main.js, che webpack classifica come blocco initial, che include i moduli main.js e ./my-function.js.
  • Il blocco async, che include solo form-validation.js (contenente un hash del file nel nome della risorsa, se configurato). Questo blocco viene scaricato solo se e quando condition è truthy.

Questa configurazione consente di posticipare il caricamento del blocco form-validation.js finché non è effettivamente necessario. Ciò può migliorare la reattività del caricamento riducendo il tempo di valutazione dello script durante il caricamento iniziale della pagina. Il download e la valutazione dello script per il blocco form-validation.js si verificano quando viene soddisfatta una condizione specificata, nel qual caso viene scaricato il modulo importato dinamicamente. Un esempio potrebbe essere una condizione in cui un polyfill viene scaricato solo per un determinato browser o, come nell'esempio precedente, il modulo importato è necessario per un'interazione dell'utente.

D'altra parte, la modifica della configurazione SplitChunksPlugin per specificare chunks: initial garantisce che il codice venga suddiviso solo in blocchi iniziali. Si tratta di chunk come quelli importati staticamente o elencati nella proprietà entry di webpack. Se esaminiamo l'esempio precedente, il blocco risultante sarebbe una combinazione di form-validation.js e main.js in un unico file di script, con conseguente potenziale peggioramento delle prestazioni di caricamento della pagina iniziale.

Le opzioni per SplitChunksPlugin possono anche essere configurate per separare script più grandi in più script più piccoli, ad esempio utilizzando l'opzione maxSize per indicare a webpack di dividere i chunk in file separati se superano quanto specificato da maxSize. Dividere i file di script di grandi dimensioni in file più piccoli può migliorare la reattività del caricamento, in quanto in alcuni casi la valutazione degli script che richiedono un uso intensivo della CPU viene suddivisa in attività più piccole, che hanno meno probabilità di bloccare il thread principale per periodi di tempo più lunghi.

Inoltre, la generazione di file JavaScript di dimensioni maggiori significa anche che è più probabile che gli script subiscano l'invalidamento della cache. Ad esempio, se spedisci uno script molto grande con il codice dell'applicazione di framework e di prima parte, l'intero bundle può essere invalidato se viene aggiornato solo il framework, ma nient'altro nella risorsa in bundle.

D'altra parte, i file di script più piccoli aumentano la probabilità che un visitatore di ritorno recuperi le risorse dalla cache, con conseguente caricamento più rapido delle pagine nelle visite ripetute. Tuttavia, i file più piccoli traggono meno vantaggio dalla compressione rispetto a quelli più grandi e potrebbero aumentare il tempo di andata e ritorno della rete durante i caricamenti di pagina con una cache del browser non inizializzata. È necessario prestare attenzione a trovare un equilibrio tra efficienza della memorizzazione nella cache, efficacia della compressione e tempo di valutazione dello script.

webpack demo

webpack SplitChunksPlugin demo.

Verifica le tue conoscenze

Quale tipo di istruzione import viene utilizzato quando si esegue la suddivisione del codice?

Dinamico import().
Esatto!
Static import.
Riprova.

Quale tipo di import istruzione deve trovarsi all'inizio di un modulo JavaScript e in nessun altro punto?

Dinamico import().
Riprova.
Static import.
Esatto!

Quando utilizzi SplitChunksPlugin in webpack, qual è la differenza tra un chunk async e un chunk initial?

I chunk async vengono caricati utilizzando import() dinamico e i chunk initial vengono caricati utilizzando import statico.
Esatto!
I chunk async vengono caricati utilizzando import statici e i chunk initial vengono caricati utilizzando import() dinamici.
Riprova.

A seguire: immagini con caricamento lento ed elementi <iframe>

Sebbene tenda a essere un tipo di risorsa piuttosto costoso, JavaScript non è l'unico tipo di risorsa di cui puoi posticipare il caricamento. Gli elementi immagine e <iframe> sono risorse potenzialmente costose di per sé. Come per JavaScript, puoi rimandare il caricamento delle immagini e dell'elemento <iframe> tramite il lazy loading, che viene spiegato nel modulo successivo di questo corso.