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 dinamicheimport()
.chunks: initial
si riferisce alle chiamate staticheimport
.chunks: all
copre sia le importazioni dinamicheimport()
che quelle statiche, consentendoti di condividere i blocchi tra le importazioniasync
einitial
.
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 bloccoinitial
, che include i modulimain.js
e./my-function.js
. - Il blocco
async
, che include soloform-validation.js
(contenente un hash del file nel nome della risorsa, se configurato). Questo blocco viene scaricato solo se e quandocondition
è 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?
import()
.import
.
Quale tipo di import
istruzione deve trovarsi all'inizio
di un modulo JavaScript e in nessun altro punto?
import()
.import
.
Quando utilizzi SplitChunksPlugin
in webpack, qual è la
differenza tra un chunk async
e un
chunk initial
?
async
vengono caricati utilizzando import()
dinamico
e i chunk initial
vengono caricati utilizzando import
statico.
async
vengono caricati utilizzando import
statici
e i chunk initial
vengono caricati utilizzando import()
dinamici.
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.