JavaScript con suddivisione del codice

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

Quando una pagina scarica, analizza e compila file JavaScript di grandi dimensioni, può non rispondere per determinati periodi di tempo. Gli elementi della pagina sono visibili, poiché fanno parte del codice HTML iniziale di una pagina e hanno uno stile CSS. Tuttavia, poiché il codice JavaScript necessario alla base di questi elementi interattivi, così come altri script caricati dalla pagina, potrebbe eseguire l'analisi e l'esecuzione del codice JavaScript affinché questi funzioni. Il risultato è che l'utente può avere la sensazione che l'interazione sia molto ritardata o addirittura interrotta.

Questo accade spesso perché il thread principale è bloccato, in quanto JavaScript viene analizzato e compilato nel thread principale. Se questo processo richiede troppo tempo, gli elementi di pagina interattivi potrebbero non rispondere abbastanza rapidamente all'input utente. Un rimedio a questo problema consiste nel caricare solo il codice JavaScript necessario per il funzionamento della pagina, posticipando il caricamento di altro codice JavaScript mediante una tecnica nota come suddivisione del codice. Questo modulo si concentra sull'ultima 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 ha esito negativo quando richiede più di 3, 5 secondi. Un'eccessiva analisi e esecuzione di JavaScript è un problema potenziale in qualsiasi momento del ciclo di vita della pagina, in quanto può aumentare il ritardo di input di un'interazione se il momento in cui l'utente interagisce con la pagina coincide con il momento in cui sono in esecuzione le principali attività thread responsabili dell'elaborazione e dell'esecuzione di JavaScript.

Inoltre, un'eccessiva esecuzione e analisi di JavaScript è particolarmente problematica durante il caricamento iniziale della pagina, poiché è il momento del ciclo di vita della pagina in cui è piuttosto probabile che gli utenti interagiscano con la pagina. Infatti, il tempo di blocco totale (TBT), una metrica relativa alla reattività al caricamento, è altamente correlata con INP, suggerendo che gli utenti hanno un'elevata tendenza a tentare interazioni durante il caricamento iniziale della pagina.

Il controllo Lighthouse, che segnala il tempo dedicato all'esecuzione di ogni file JavaScript richiesto dalla pagina, è utile in quanto può aiutarti a identificare esattamente quali script potrebbero essere candidati per la suddivisione del codice. Puoi quindi andare oltre utilizzando lo strumento di copertura in Chrome DevTools per identificare esattamente quali parti del codice 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. Ti consente di suddividere un bundle JavaScript in due parti:

  • Il codice JavaScript necessario al caricamento della pagina, pertanto non può essere caricato in nessun altro momento.
  • JavaScript rimanente che può essere caricato in un secondo momento, il più delle volte nel punto 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 <script> elementi che richiedono una determinata risorsa JavaScript durante l'avvio, effettua una richiesta per una risorsa JavaScript in un momento successivo 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 emette uno qualsiasi dei campi <input> di un modulo. In questo caso, la risorsa JavaScript responsabile della gestione della logica di convalida del modulo è coinvolta nella pagina solo quando è più probabile che venga effettivamente utilizzata.

I bundler JavaScript come webpack, Parcel, Rollup ed esbuild possono essere configurati per suddividere i bundle JavaScript in blocchi più piccoli ogni volta che riscontrano una chiamata import() dinamica nel codice sorgente. La maggior parte di questi strumenti esegue questa operazione automaticamente, ma esbuild in particolare richiede l'attivazione di 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 alcuni aspetti se decidi di controllare il tuo codice sorgente JavaScript per individuare opportunità di suddivisione del codice.

Se puoi, utilizza un bundler

È prassi comune per gli sviluppatori utilizzare i moduli JavaScript durante il processo di sviluppo. È un eccellente miglioramento dell'esperienza di sviluppo che migliora la leggibilità e la manutenibilità del codice. Tuttavia, è possibile che si verifichino alcune caratteristiche prestazionali non ottimali quando i moduli JavaScript vengono spediti in produzione.

In particolare, devi utilizzare un bundler per elaborare e ottimizzare il codice sorgente, inclusi i moduli su cui intendi suddividere il codice. I bundler sono molto efficaci non solo nell'applicare ottimizzazioni al codice sorgente JavaScript, ma sono anche abbastanza efficaci nel bilanciare considerazioni sulle prestazioni, come le dimensioni del bundle rispetto al rapporto di compressione. L'efficacia della compressione aumenta con le dimensioni dei bundle, ma i bundle cercano anche di fare in modo che i bundle non siano così grandi da dover sostenere attività lunghe a causa della valutazione degli script.

I bundler evitano anche il problema di spedire sulla rete un numero elevato di moduli non in bundle. Le architetture che utilizzano moduli JavaScript tendono ad avere strutture di moduli grandi e complesse. Quando le strutture di moduli non sono raggruppate, ogni modulo rappresenta una richiesta HTTP separata e l'interattività nella tua app web potrebbe subire ritardi se non raggruppi i moduli. Sebbene sia possibile utilizzare il suggerimento delle risorse <link rel="modulepreload"> per caricare il prima possibile strutture di moduli di grandi dimensioni, i bundle JavaScript sono comunque preferibili dal punto di vista delle prestazioni di caricamento.

Non disabilitare inavvertitamente la compilazione dei flussi di dati

Il motore JavaScript V8 di Chromium offre una serie di ottimizzazioni pronte all'uso per garantire che il codice JavaScript di produzione venga caricato nel modo più efficiente possibile. Una di queste ottimizzazioni è nota come compilation di flussi di dati che, come l'analisi incrementale dell'HTML trasmesso in streaming al browser, compilano blocchi di JavaScript in streaming non appena arrivano dalla rete.

Esistono due modi per garantire che la compilazione dei flussi di dati venga eseguita per la tua applicazione web in Chromium:

  • Trasforma il tuo codice di produzione per evitare di utilizzare moduli JavaScript. I bundle possono trasformare il codice sorgente JavaScript in base a una destinazione di compilazione e la destinazione è spesso specifica per un determinato ambiente. La versione V8 applicherà la compilazione dei flussi di dati a qualsiasi codice JavaScript che non utilizza moduli e potrai 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 inviare moduli JavaScript in produzione, utilizza l'estensione .mjs. Indipendentemente dal fatto che il codice JavaScript di produzione utilizzi i moduli, non esiste un tipo di contenuto speciale per JavaScript che utilizza i moduli rispetto a JavaScript che non lo fa. Per quanto riguarda la versione V8, di fatto disattivi la compilazione dei flussi di dati quando distribuisci i moduli JavaScript in produzione utilizzando l'estensione .js. Se utilizzi l'estensione .mjs per i moduli JavaScript, V8 può garantire che la compilazione di flussi di dati per il codice JavaScript basato su moduli non venga interrotta.

Non lasciare che queste considerazioni ti scoraggino dall'utilizzare la suddivisione del codice. La suddivisione del codice è un modo efficace per ridurre i payload JavaScript iniziali agli utenti, ma utilizzando un bundler e sapere come puoi preservare il comportamento di compilazione dei flussi di dati di V8, puoi garantire che il tuo codice JavaScript di produzione sia il più veloce possibile per gli utenti.

Demo sull'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() che import statiche. Il comportamento di SplitChunksPlugin può essere modificato specificando l'opzione chunks nella sua configurazione:

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

Per impostazione predefinita, ogni volta che webpack rileva un'istruzione import() dinamica, crea un blocco separato per il modulo in questione:

/* 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 predefinita del pacchetto web per lo snippet di codice precedente genera due blocchi 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 configurata). Questo blocco viene scaricato solo se e quando condition è truthy.

Questa configurazione consente di rimandare il caricamento del blocco form-validation.js fino a quando non è effettivamente necessario. Ciò può migliorare la reattività al caricamento riducendo il tempo di valutazione degli script durante il caricamento iniziale della pagina. Il download e la valutazione degli script per il blocco form-validation.js si verificano quando una condizione specificata è soddisfatta, 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 utente.

D'altra parte, la modifica della configurazione SplitChunksPlugin per specificare chunks: initial garantisce che il codice venga suddiviso solo nei blocchi iniziali. Si tratta di blocchi, come quelli importati in modo statico o elencati nella proprietà entry di webpack. Guardando l'esempio precedente, il blocco risultante sarebbe una combinazione di form-validation.js e main.js in un unico file di script, con un conseguente rendimento del caricamento iniziale della pagina potenzialmente inferiore.

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 al webpack di suddividere i blocchi in file separati se superano quanto specificato da maxSize. La suddivisione dei file di script di grandi dimensioni in file più piccoli può migliorare la reattività al caricamento, poiché in alcuni casi il lavoro di valutazione degli script che richiede molta CPU è suddiviso 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 comporta anche una maggiore probabilità di annullamento della convalida della cache per gli script. Ad esempio, se distribuisci uno script molto grande con il framework e il codice dell'applicazione proprietaria, l'intero bundle può essere invalidato se viene aggiornato solo il framework e nient'altro nella risorsa in bundle.

Invece, i file di script più piccoli aumentano la probabilità che un visitatore di ritorno recuperi le risorse dalla cache, determinando un caricamento più rapido delle pagine in caso di visite ripetute. Tuttavia, i file più piccoli traggono beneficio dalla compressione rispetto ai file più grandi e possono aumentare il tempo di round trip della rete nei caricamenti delle pagine con una cache del browser non pronta. È necessario fare attenzione a trovare un equilibrio tra efficienza della memorizzazione nella cache, efficacia della compressione e tempo di valutazione degli script.

demo webpack

demo webpack SplitChunksPlugin.

verifica le tue conoscenze

Quale tipo di istruzione import viene utilizzato durante la suddivisione del codice?

import() dinamico.
risposta esatta.
import statico.
Riprova.

Quale tipo di istruzione import deve trovarsi nella parte superiore di un modulo JavaScript e in nessun'altra posizione?

import() dinamico.
Riprova.
import statico.
risposta esatta.

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

I blocchi async vengono caricati utilizzando i blocchi import() dinamici, mentre i blocchi initial vengono caricati utilizzando import statico.
risposta esatta.
I blocchi async vengono caricati utilizzando import statici, mentre i blocchi initial vengono caricati utilizzando import() dinamico.
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 a sé stanti. Come per JavaScript, puoi rimandare il caricamento delle immagini e dell'elemento <iframe> caricandoli tramite caricamento lento, come spiegato nel modulo successivo di questo corso.