Valutazione degli script e attività lunghe

Durante il caricamento degli script, il browser impiega del tempo per valutarli prima dell'esecuzione, il che può causare attività lunghe. Scopri come funziona la valutazione degli script e cosa puoi fare per evitare che causi attività lunghe durante il caricamento della pagina.

Quando si tratta di ottimizzare l'Interaction to Next Paint (INP), la maggior parte dei consigli che troverai riguarda l'ottimizzazione delle interazioni stesse. Ad esempio, nella guida all'ottimizzazione delle attività lunghe vengono illustrate tecniche come la cessione con setTimeout e altre. Queste tecniche sono utili perché consentono al thread principale di avere un po' di respiro evitando attività lunghe, il che può consentire a interazioni e altre attività di essere eseguite prima, anziché dover attendere una singola attività lunga.

Tuttavia, cosa succede alle attività di lunga durata derivanti dal caricamento degli script stessi? Queste attività possono interferire con le interazioni degli utenti e influire sull'INP di una pagina durante il caricamento. Questa guida esplorerà il modo in cui i browser gestiscono le attività avviate dalla valutazione degli script e analizzerà cosa puoi fare per suddividere il lavoro di valutazione degli script in modo che il thread principale possa rispondere meglio all'input dell'utente durante il caricamento della pagina.

Che cos'è la valutazione degli script?

Se hai profilato un'applicazione che include molto JavaScript, potresti aver notato attività lunghe in cui la causa è etichettata come Valuta script.

Lavoro di valutazione dello script visualizzato nel Profiler delle prestazioni di Chrome DevTools. Il lavoro causa un'attività lunga durante l'avvio, che blocca la capacità del thread principale di rispondere alle interazioni dell'utente.
La valutazione degli script funziona come mostrato nel Profiler delle prestazioni in Chrome DevTools. In questo caso, il lavoro è sufficiente a causare un'attività lunga che impedisce al thread principale di svolgere altre attività, incluse quelle che guidano le interazioni degli utenti.

La valutazione dello script è una parte necessaria dell'esecuzione di JavaScript nel browser, poiché JavaScript viene compilato just-in-time prima dell'esecuzione. Quando uno script viene valutato, viene prima analizzato per rilevare eventuali errori. Se il parser non rileva errori, lo script viene compilato in bytecode e può continuare l'esecuzione.

Sebbene necessaria, la valutazione degli script può essere problematica, in quanto gli utenti potrebbero tentare di interagire con una pagina poco dopo il rendering iniziale. Tuttavia, il semplice fatto che una pagina sia stata renderizzata non significa che il caricamento sia terminato. Le interazioni che si verificano durante il caricamento possono essere ritardate perché la pagina è impegnata a valutare gli script. Anche se non è garantito che un'interazione possa avvenire in questo momento, poiché uno script responsabile potrebbe non essere ancora stato caricato, potrebbero esserci interazioni dipendenti da JavaScript che sono pronte oppure l'interattività non dipende affatto da JavaScript.

Relazione tra gli script e le attività che li valutano

L'avvio delle attività responsabili della valutazione dello script dipende dal fatto che lo script che stai caricando venga caricato con un tipico elemento <script> o se lo script è un modulo caricato con type=module. Poiché i browser tendono a gestire le cose in modo diverso, verrà trattato il modo in cui i principali motori dei browser gestiscono la valutazione degli script, in quanto i comportamenti di valutazione degli script variano.

Script caricati con l'elemento <script>

Il numero di attività inviate per valutare gli script è generalmente correlato direttamente al numero di elementi <script> in una pagina. Ogni elemento <script> avvia un'attività per valutare lo script richiesto in modo che possa essere analizzato, compilato ed eseguito. Questo vale per i browser basati su Chromium, Safari e Firefox.

Perché è importante? Supponiamo che tu stia utilizzando un bundler per gestire gli script di produzione e che tu l'abbia configurato per raggruppare tutto ciò che serve alla tua pagina per essere eseguita in un unico script. Se questo è il caso del tuo sito web, puoi prevedere che verrà inviata una singola attività per valutare lo script. È una cosa brutta? Non necessariamente, a meno che lo script non sia enorme.

Puoi suddividere il lavoro di valutazione degli script evitando di caricare grandi blocchi di JavaScript e caricare script più piccoli e individuali utilizzando elementi <script> aggiuntivi.

Anche se devi sempre cercare di caricare la minor quantità possibile di JavaScript durante il caricamento della pagina, la suddivisione degli script garantisce che, invece di un'unica attività di grandi dimensioni che potrebbe bloccare il thread principale, avrai un numero maggiore di attività più piccole che non bloccheranno il thread principale o almeno lo bloccheranno meno di prima.

Più attività che comportano la valutazione degli script, come visualizzato nel Profiler delle prestazioni di Chrome DevTools. Poiché vengono caricati più script di dimensioni ridotte anziché pochi script di dimensioni maggiori, è meno probabile che le attività diventino lunghe, consentendo al thread principale di rispondere più rapidamente all&#39;input utente.
Sono stati generati più task per valutare gli script a causa della presenza di più elementi <script> nel codice HTML della pagina. Questa soluzione è preferibile all'invio agli utenti di un unico bundle di script di grandi dimensioni, che ha maggiori probabilità di bloccare il thread principale.

Puoi considerare la suddivisione delle attività per la valutazione degli script come qualcosa di simile al rendimento durante i callback degli eventi eseguiti durante un'interazione. Tuttavia, con la valutazione degli script, il meccanismo di yielding suddivide il codice JavaScript caricato in più script più piccoli, anziché in un numero inferiore di script più grandi che hanno maggiori probabilità di bloccare il thread principale.

Script caricati con l'elemento <script> e l'attributo type=module

Ora è possibile caricare i moduli ES in modo nativo nel browser con l'attributo type=module sull'elemento <script>. Questo approccio al caricamento degli script offre alcuni vantaggi per l'esperienza degli sviluppatori, ad esempio non dover trasformare il codice per l'utilizzo in produzione, soprattutto se utilizzato in combinazione con le mappe di importazione. Tuttavia, il caricamento degli script in questo modo pianifica attività che variano da browser a browser.

Browser basati su Chromium

Nei browser come Chrome o quelli derivati, il caricamento dei moduli ES utilizzando l'attributo type=module produce tipi di attività diversi da quelli che normalmente vedresti quando non utilizzi type=module. Ad esempio, verrà eseguita un'attività per ogni script del modulo che prevede un'attività etichettata come Compila modulo.

Compilazione dei moduli in più attività visualizzata in Chrome DevTools.
Comportamento di caricamento dei moduli nei browser basati su Chromium. Ogni script del modulo genererà una chiamata Compile module per compilare i contenuti prima della valutazione.

Una volta compilati i moduli, qualsiasi codice eseguito successivamente al loro interno attiverà un'attività etichettata come Valuta modulo.

Valutazione just-in-time di un modulo visualizzata nel pannello delle prestazioni di Chrome DevTools.
Quando viene eseguito il codice in un modulo, questo viene valutato just-in-time.

L'effetto qui, almeno in Chrome e nei browser correlati, è che i passaggi di compilazione vengono suddivisi quando si utilizzano i moduli ES. Si tratta di un chiaro vantaggio in termini di gestione delle attività lunghe. Tuttavia, il lavoro di valutazione del modulo risultante comporta comunque alcuni costi inevitabili. Sebbene tu debba cercare di distribuire il minor codice JavaScript possibile, l'utilizzo dei moduli ES, indipendentemente dal browser, offre i seguenti vantaggi:

  • Tutto il codice del modulo viene eseguito automaticamente in modalità StrictMode, il che consente potenziali ottimizzazioni da parte dei motori JavaScript che altrimenti non potrebbero essere eseguite in un contesto non rigido.
  • Gli script caricati utilizzando type=module vengono trattati come se fossero differiti per impostazione predefinita. È possibile utilizzare l'attributo async negli script caricati con type=module per modificare questo comportamento.

Safari e Firefox

Quando i moduli vengono caricati in Safari e Firefox, ognuno viene valutato in un'attività separata. Ciò significa che in teoria potresti caricare un singolo modulo di primo livello costituito solo da istruzioni statiche import in altri moduli e ogni modulo caricato comporterà una richiesta di rete e un'attività separate per la sua valutazione.

Script caricati con import() dinamico

import() dinamico è un altro metodo per caricare gli script. A differenza delle istruzioni import statiche che devono trovarsi all'inizio di un modulo ES, una chiamata import() dinamica può essere visualizzata ovunque in uno script per caricare un blocco di JavaScript on demand. Questa tecnica è chiamata suddivisione del codice.

import() dinamico offre due vantaggi per migliorare l'INP:

  1. I moduli il cui caricamento viene posticipato riducono la contesa del thread principale durante l'avvio, diminuendo la quantità di JavaScript caricato in quel momento. In questo modo, il thread principale è libero di rispondere più rapidamente alle interazioni degli utenti.
  2. Quando vengono effettuate chiamate dinamiche import(), ogni chiamata separa in modo efficace la compilazione e la valutazione di ogni modulo nella propria attività. Naturalmente, un import() dinamico che carica un modulo molto grande avvierà un'attività di valutazione dello script piuttosto grande, che può interferire con la capacità del thread principale di rispondere all'input dell'utente se l'interazione si verifica contemporaneamente alla chiamata import() dinamica. Pertanto, è ancora molto importante caricare la minor quantità possibile di JavaScript.

Le chiamate dinamiche import() si comportano in modo simile in tutti i principali motori del browser: le attività di valutazione dello script risultanti saranno uguali alla quantità di moduli importati dinamicamente.

Script caricati in un web worker

I web worker sono un caso d'uso speciale di JavaScript. I web worker vengono registrati nel thread principale e il codice all'interno del worker viene eseguito nel proprio thread. Ciò è estremamente vantaggioso in quanto, mentre il codice che registra il web worker viene eseguito sul thread principale, il codice all'interno del web worker non lo è. In questo modo si riduce la congestione del thread principale e si può contribuire a mantenere il thread principale più reattivo alle interazioni degli utenti.

Oltre a ridurre il lavoro del thread principale, i web worker stessi possono caricare script esterni da utilizzare nel contesto del worker, tramite importScripts o istruzioni import statiche nei browser che supportano i module worker. Il risultato è che qualsiasi script richiesto da un web worker viene valutato al di fuori del thread principale.

Compromessi e considerazioni

Sebbene la suddivisione degli script in file separati più piccoli contribuisca a limitare le attività lunghe rispetto al caricamento di file più grandi, è importante tenere conto di alcuni aspetti quando si decide come suddividere gli script.

Efficienza di compressione

La compressione è un fattore da considerare quando si suddividono gli script. Quando gli script sono più piccoli, la compressione diventa un po' meno efficiente. Gli script più grandi trarranno molto più vantaggio dalla compressione. Sebbene l'aumento dell'efficienza della compressione contribuisca a mantenere i tempi di caricamento degli script il più bassi possibile, è necessario trovare un equilibrio per assicurarsi di suddividere gli script in blocchi più piccoli per facilitare una migliore interattività durante l'avvio.

I bundler sono strumenti ideali per gestire le dimensioni dell'output degli script da cui dipende il tuo sito web:

  • Per quanto riguarda webpack, il plug-in SplitChunksPlugin può essere utile. Consulta la documentazione di SplitChunksPlugin per le opzioni che puoi impostare per gestire le dimensioni degli asset.
  • Per altri bundler come Rollup ed esbuild, puoi gestire le dimensioni dei file di script utilizzando chiamate import() dinamiche nel codice. Questi bundler, così come webpack, separano automaticamente l'asset importato dinamicamente nel proprio file, evitando così bundle iniziali di dimensioni maggiori.

Annullamento convalida cache

L'invalidazione della cache svolge un ruolo importante nella velocità di caricamento di una pagina durante le visite ripetute. Quando spedisci bundle di script di grandi dimensioni e monolitici, sei in una posizione di svantaggio per quanto riguarda la memorizzazione nella cache del browser. Questo perché quando aggiorni il codice proprietario, ad esempio aggiornando i pacchetti o correggendo i bug, l'intero bundle viene invalidato e deve essere scaricato di nuovo.

Suddividendo gli script, non solo suddividi il lavoro di valutazione degli script in attività più piccole, ma aumenti anche la probabilità che i visitatori di ritorno recuperino più script dalla cache del browser anziché dalla rete. Ciò si traduce in un caricamento della pagina complessivamente più rapido.

Moduli nidificati e prestazioni di caricamento

Se spedisci moduli ES in produzione e li carichi con l'attributo type=module, devi essere consapevole di come l'annidamento dei moduli può influire sul tempo di avvio. Il nesting dei moduli si verifica quando un modulo ES importa staticamente un altro modulo ES che importa staticamente un altro modulo ES:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Se i moduli ES non sono raggruppati, il codice precedente genera una catena di richieste di rete: quando a.js viene richiesto da un elemento <script>, viene inviata un'altra richiesta di rete per b.js, che a sua volta comporta un'altra richiesta per c.js. Un modo per evitarlo è utilizzare un bundler, ma assicurati di configurarlo in modo da suddividere gli script per distribuire il lavoro di valutazione degli script.

Se non vuoi utilizzare un bundler, un altro modo per aggirare le chiamate ai moduli nidificati è utilizzare il suggerimento per le risorse modulepreload, che precarica i moduli ES in anticipo per evitare catene di richieste di rete.

Conclusione

L'ottimizzazione della valutazione degli script nel browser è senza dubbio un'impresa difficile. L'approccio dipende dai requisiti e dai vincoli del tuo sito web. Tuttavia, dividendo gli script, distribuisci il lavoro di valutazione degli script su numerose attività più piccole, dando così al thread principale la possibilità di gestire le interazioni degli utenti in modo più efficiente, anziché bloccarlo.

Per ricapitolare, ecco alcune cose che puoi fare per suddividere le attività di valutazione degli script di grandi dimensioni:

  • Quando carichi script utilizzando l'elemento <script> senza l'attributo type=module, evita di caricare script di grandi dimensioni, in quanto attiveranno attività di valutazione degli script che richiedono molte risorse e bloccano il thread principale. Distribuisci i tuoi script su più elementi <script> per suddividere il lavoro.
  • L'utilizzo dell'attributo type=module per caricare i moduli ES in modo nativo nel browser avvierà attività individuali per la valutazione di ogni script di modulo separato.
  • Riduci le dimensioni dei bundle iniziali utilizzando le chiamate dinamiche import(). Funziona anche nei bundler, che trattano ogni modulo importato dinamicamente come un "punto di divisione", generando uno script separato per ogni modulo importato dinamicamente.
  • Assicurati di valutare i compromessi, ad esempio l'efficienza della compressione e l'invalidazione della cache. Gli script più grandi verranno compressi meglio, ma è più probabile che comportino un lavoro di valutazione degli script più costoso in un numero inferiore di attività e che comportino l'invalidazione della cache del browser, con conseguente efficienza di memorizzazione nella cache complessivamente inferiore.
  • Se utilizzi i moduli ES in modo nativo senza raggruppamento, utilizza il suggerimento per le risorse modulepreload per ottimizzarne il caricamento durante l'avvio.
  • Come sempre, spedisci il minor quantitativo possibile di JavaScript.

È sicuramente un equilibrio delicato, ma suddividendo gli script e riducendo i payload iniziali con import() dinamici, puoi ottenere prestazioni di avvio migliori e adattarti meglio alle interazioni degli utenti durante questo periodo di avvio cruciale. In questo modo, dovresti ottenere un punteggio migliore per la metrica INP, offrendo così una migliore esperienza utente.