Valutazione degli script e attività lunghe

Durante il caricamento degli script, il browser potrebbe impiegare 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 provochi attività lunghe durante il caricamento della pagina.

Per quanto riguarda l'ottimizzazione di Interaction to Next Paint (INP), la maggior parte dei consigli che puoi ottenere è quella di ottimizzare le interazioni in prima persona. Ad esempio, nella guida per l'ottimizzazione delle attività lunghe, vengono descritte tecniche come la resa con setTimeout, isInputPending e così via. Queste tecniche sono utili perché consentono al thread principale di avere una certa respirazione evitando attività lunghe, il che può consentire di avere più opportunità di interazioni e altre attività di essere eseguite prima anziché dover attendere una singola attività lunga.

Tuttavia, che dire delle attività lunghe 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 analizzerà il modo in cui i browser gestiscono le attività avviate dalla valutazione degli script e scopri cosa puoi fare per interrompere il lavoro di valutazione degli script in modo che il thread principale sia più reattivo all'input dell'utente durante il caricamento della pagina.

Che cos'è la valutazione degli script?

Se hai profilato un'applicazione che fornisce molto codice JavaScript, potresti aver visto attività lunghe per cui il responsabile è etichettato come Valuta script.

La valutazione degli script funziona come viene visualizzata nel profiler delle prestazioni di Chrome DevTools. Questa operazione causa un'attività lunga durante l'avvio, che blocca la capacità del thread principale di rispondere alle interazioni degli utenti.
La valutazione degli script funziona come mostrato nel profiler delle prestazioni in Chrome DevTools. In questo caso, il lavoro è sufficiente per causare un'attività lunga che impedisca al thread principale di svolgere altre attività, incluse le attività che favoriscono le interazioni degli utenti.

La valutazione degli 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 dapprima analizzato la presenza di errori. Se l'analizzatore sintattico non trova errori, lo script viene compilato in bytecode e l'esecuzione può continuare.

Sebbene sia necessario, la valutazione degli script può risultare problematica, in quanto gli utenti potrebbero provare a interagire con una pagina poco dopo la sua prima visualizzazione. 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 subire ritardi perché la pagina è impegnata a valutare gli script. Sebbene non sia garantito in questo momento l'interazione desiderata, poiché uno script responsabile potrebbe non essere ancora stato caricato, potrebbero esserci interazioni dipendenti da JavaScript che sono pronte oppure l'interattività non dipende da JavaScript.

La relazione tra gli script e le attività che li valutano

Il modo in cui le attività responsabili della valutazione degli script vengono avviate a seconda che lo script che stai caricando venga caricato tramite un normale elemento <script> o che lo script sia un modulo caricato con type=module. Poiché i browser hanno la tendenza a gestire le cose in modo diverso, vedremo il modo in cui i principali motori dei browser gestiscono la valutazione degli script e dove variano i comportamenti di valutazione degli script.

Caricamento di script con l'elemento <script> in corso...

Il numero di attività inviate per valutare gli script in genere ha una relazione diretta con il numero di elementi <script> su una pagina. Ogni elemento <script> avvia un'attività per valutare lo script richiesto in modo che possa essere analizzato, compilato ed eseguito. Questo è il caso dei browser basati su Chromium, Safari e Firefox.

Perché tenerne conto? Supponiamo che utilizzi un bundler per gestire i tuoi script di produzione e che lo abbia configurato in modo da raggruppare tutto ciò di cui la pagina ha bisogno per essere eseguiti in un unico script. Se questo è il caso del tuo sito web, puoi aspettarti che venga inviata una singola attività per valutare lo script. È una cosa brutta? Non necessariamente, a meno che la scrittura non sia enigma.

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

Anche se devi sempre cercare di caricare il minor numero di JavaScript possibile durante il caricamento della pagina, la suddivisione degli script garantisce che, anziché un'unica attività di grandi dimensioni che potrebbe bloccare il thread principale, avrai un numero maggiore di attività minori che non bloccano affatto il thread principale o almeno quest'ultimo rispetto a quello con cui hai iniziato.

Più attività che implicano la valutazione degli script visualizzate nel profiler delle prestazioni di Chrome DevTools. Poiché vengono caricati più script più piccoli anziché un numero minore di script più grandi, è meno probabile che le attività diventino attività lunghe, consentendo al thread principale di rispondere più rapidamente all&#39;input dell&#39;utente.
Sono state generate diverse attività per valutare gli script a causa della presenza di più elementi <script> nel codice HTML della pagina. È preferibile inviare agli utenti un unico pacchetto di script di grandi dimensioni, in quanto è più probabile che il thread principale venga bloccato.

La suddivisione delle attività per la valutazione dello script è simile al rendimento durante i callback eventi eseguiti durante un'interazione. Tuttavia, durante la valutazione degli script, il meccanismo di rendimento suddivide il codice JavaScript caricato in più script più piccoli, anziché in un numero minore di script più grandi rispetto a quelli che hanno maggiori probabilità di bloccare il thread principale.

Caricamento di script 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 gli sviluppatori, ad esempio la possibilità di non trasformare il codice per l'uso in produzione, soprattutto se utilizzato in combinazione con le mappe di importazione. Tuttavia, il caricamento degli script in questo modo pianifica attività che differiscono da un browser all'altro.

Browser basati su Chromium

Nei browser come Chrome (o in quelli da essi derivati), il caricamento di moduli ES mediante l'attributo type=module produce tipi di attività diversi rispetto a quelli normalmente visualizzati quando non si utilizza type=module. Ad esempio, per ogni script del modulo viene eseguita un'attività che prevede un'attività etichettata come Compila modulo.

La compilazione dei moduli funziona in più attività come viene visualizzata in Chrome DevTools.
Comportamento di caricamento dei moduli nei browser basati su Chromium. Ogni script del modulo genererà una chiamata Compila modulo per compilare i relativi contenuti prima della valutazione.

Una volta compilati i moduli, qualsiasi codice eseguito successivamente attiverà l'attività etichettata come Valuta modulo.

Valutazione just-in-time di un modulo come visualizzato nel riquadro delle prestazioni di Chrome DevTools.
Quando il codice viene eseguito in un modulo, quest'ultimo verrà valutato just-in-time.

L'effetto qui, almeno in Chrome e nei browser correlati, è che i passaggi di compilazione vengono suddivisi quando si utilizzano moduli ES. Questa è una chiara vittoria in termini di gestione di attività lunghe; tuttavia, il conseguente lavoro di valutazione dei moduli che ne risulta comporta comunque un costo inevitabile. Anche se dovresti cercare di fornire la minor quantità di 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à rigida, che consente potenziali ottimizzazioni da parte dei motori JavaScript che altrimenti non potrebbero essere effettuate in un contesto non rigido.
  • Gli script caricati utilizzando type=module vengono trattati come se fossero stati posticipati 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, ciascuno di essi viene valutato in un'attività separata. Ciò significa che, in teoria, è possibile caricare un singolo modulo di primo livello composto solo da istruzioni statiche import in altri moduli e ogni modulo caricato comporta una richiesta di rete e un'attività separate per la valutazione.

Caricamento degli script con import() dinamico in corso...

Dinamico import() è un altro metodo per caricare gli script. A differenza delle istruzioni import statiche che devono essere presenti nella parte superiore di un modulo ES, una chiamata import() dinamica può apparire ovunque in uno script per caricare un blocco di JavaScript on demand. Questa tecnica è chiamata suddivisione del codice.

L'import() dinamico presenta due vantaggi per quanto riguarda il miglioramento dell'INP:

  1. I moduli che vengono caricati in un secondo momento riducono la contesa dei thread principali durante l'avvio diminuendo la quantità di JavaScript caricato in quel momento. Questo libera il thread principale in modo che possa essere più reattivo alle interazioni degli utenti.
  2. Quando vengono effettuate chiamate import() dinamiche, ogni chiamata separa effettivamente la compilazione e la valutazione di ogni modulo per la propria attività. Naturalmente, un import() dinamico che carica un modulo di grandi dimensioni avvia un'attività di valutazione dello script piuttosto grande e questo può interferire con la capacità del thread principale di rispondere all'input dell'utente se l'interazione si verifica contemporaneamente alla chiamata dinamica import(). Pertanto, è comunque molto importante caricare il minor numero di JavaScript possibile.

Le chiamate import() dinamiche si comportano in modo simile in tutti i principali motori dei browser: le attività di valutazione degli script risultanti saranno la stessa del numero di moduli che vengono importati in modo dinamico.

Caricamento di script in un web worker

I web worker sono un caso d'uso speciale di JavaScript. I web worker sono registrati nel thread principale e il codice all'interno del worker viene quindi eseguito nel proprio thread. Ciò è di grande aiuto nel senso che, mentre il codice che registra il web worker viene eseguito sul thread principale, il codice all'interno del web worker non viene eseguito. In questo modo si riduce la congestione dei thread principali e il thread principale diventa più reattivo alle interazioni degli utenti.

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

Vantaggi e considerazioni

Anche se la suddivisione degli script in file separati e più piccoli aiuta a limitare le attività lunghe anziché il caricamento di meno file molto più grandi, è importante tenere in considerazione alcuni aspetti quando decidi come suddividere gli script.

Efficienza di compressione

La compressione è un fattore importante nella suddivisione degli script. Quando gli script sono di dimensioni inferiori, la compressione diventa in qualche modo meno efficiente. Gli script di dimensioni maggiori saranno molto più efficaci con la compressione. L'aumento dell'efficienza della compressione contribuisce a ridurre il più possibile i tempi di caricamento degli script, ma è un po' di equilibrio per assicurarsi di suddividerli in blocchi sufficientemente più piccoli per facilitare una migliore interattività durante l'avvio.

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

  • Per quanto riguarda il webpack, il suo plug-in SplitChunksPlugin può esserti di aiuto. Consulta la documentazione di SplitChunksPlugin per conoscere 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 bundle, così come i webpack, separano automaticamente l'asset importato dinamicamente in un proprio file, evitando dimensioni iniziali per i bundle maggiori.

Annullamento convalida cache

L'annullamento della convalida della cache gioca un ruolo importante nella velocità di caricamento di una pagina in caso di visite ripetute. Quando distribuisci bundle di script monolitici di grandi dimensioni, hai uno svantaggio riguardo alla memorizzazione nella cache del browser. Questo perché quando aggiorni il codice proprietario (tramite l'aggiornamento dei pacchetti o la correzione di bug di spedizione), l'intero bundle viene invalidato e deve essere scaricato di nuovo.

Suddividendo i tuoi script, non solo suddividerai il lavoro di valutazione degli script in attività più piccole, ma aumenterai anche le probabilità che i visitatori di ritorno recuperino più script dalla cache del browser anziché dalla rete. Ciò si traduce in un caricamento pagina più rapido generale.

Moduli nidificati e prestazioni di caricamento

Se stai inviando moduli ES in produzione e li stai caricando con l'attributo type=module, devi sapere in che modo la nidificazione dei moduli può influire sui tempi di avvio. La nidificazione di moduli si riferisce a quando un modulo ES importa in modo statico un altro modulo ES che importa in modo statico un altro modulo ES:

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

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

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

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

Conclusione

Ottimizzare la valutazione degli script nel browser è senza dubbio un’impresa difficile. L'approccio dipende dai requisiti e dai vincoli del tuo sito web. Tuttavia, la suddivisione degli script consente di suddividere il lavoro di valutazione degli script in numerose attività più piccole, consentendo al thread principale di gestire le interazioni degli utenti in modo più efficiente, invece di bloccare il thread principale.

Per ricapitolare, ecco alcune cose che puoi fare per suddividere 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 dimensioni molto grandi, poiché daranno inizio ad attività di valutazione degli script che richiedono molte risorse e bloccano il thread principale. Distribuisci i tuoi script su più elementi <script> per suddividere questo lavoro.
  • L'utilizzo dell'attributo type=module per caricare moduli ES in modo nativo nel browser avvierà singole attività di valutazione per ogni script di modulo separato.
  • Riduci le dimensioni dei bundle iniziali utilizzando le chiamate import() dinamiche. Questo metodo funziona anche nei bundler, poiché i bundle tratteranno ogni modulo importato dinamicamente come un "punto di suddivisione", generando uno script separato per ogni modulo importato in modo dinamico.
  • Valuta sempre determinati compromessi, come l'efficienza della compressione e l'annullamento della convalida della cache. Gli script più grandi si comprimono meglio, ma hanno più probabilità di comportare attività di valutazione degli script più costose in meno attività e comportano l'annullamento della convalida della cache del browser, con una conseguente minore efficienza generale della memorizzazione nella cache.
  • Se utilizzi moduli ES in modo nativo senza raggruppamento, usa il suggerimento relativo alle risorse modulepreload per ottimizzare il relativo caricamento durante l'avvio.
  • Come sempre, fornisci il minor numero di codice JavaScript possibile.

È sicuramente una questione di equilibrio, ma suddividendo gli script e riducendo i payload iniziali tramite import() dinamico, puoi ottenere migliori prestazioni di avvio e soddisfare meglio le interazioni degli utenti durante quel periodo di avvio cruciale. Questo dovrebbe aiutarti a ottenere un punteggio migliore per la metrica INP, garantendo così un'esperienza utente migliore.

Immagine hero di Unsplash, di Markus Spiske.