Quando carica gli script, il browser impiega del tempo per valutarli prima dell'esecuzione, il che può causare attività lunghe. Scopri come funziona la valutazione dello 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 riceverai è di ottimizzare le interazioni stesse. Ad esempio, nella guida all'ottimizzazione delle attività lunghe vengono discusse tecniche come il rendimento con setTimeout
e altre. Queste tecniche sono vantaggiose, poiché consentono al thread principale di respirare evitando attività lunghe, il che può consentire di eseguire più rapidamente le interazioni e altre attività rispetto all'attesa per un'unica attività lunga.
Cosa accade però per le attività lunghe che derivano 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 illustra in che modo i browser gestiscono le attività avviate dalla valutazione dello script e spiega cosa puoi fare per suddividere il lavoro di valutazione dello script in modo che il thread principale possa essere più reattivo all'input dell'utente durante il caricamento della pagina.
Che cos'è la valutazione degli script?
Se hai eseguito il profiling di un'applicazione che include molto codice JavaScript, potresti aver notato attività lunghe in cui il problema è contrassegnato come Valuta script.
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 prima analizzato per rilevare eventuali errori. Se il parser non trova errori, lo script viene compilato in bytecode e può continuare l'esecuzione.
Sebbene necessaria, la valutazione dello script può essere problematica, in quanto gli utenti potrebbero provare a interagire con una pagina poco dopo il relativo rendering iniziale. Tuttavia, il semplice fatto che una pagina sia stata eseguita non significa che il caricamento sia stato completato. Le interazioni che si verificano durante il caricamento possono essere ritardate perché la pagina è impegnata a valutare gli script. Sebbene non sia garantito che un'interazione possa avvenire in questo momento, poiché lo script responsabile potrebbe non essere ancora stato caricato, potrebbero esserci interazioni dipendenti da JavaScript che sono pronte o l'interattività non dipende affatto da JavaScript.
La relazione tra gli script e le attività che li valutano
Il modo in cui vengono avviate le attività responsabili della valutazione dello script dipende dal fatto che lo script che stai caricando sia caricato con un elemento <script>
tipico o se lo script è un modulo caricato con type=module
. Dal momento che i browser tendono a gestire le cose in modo diverso, il modo in cui i principali motori dei browser gestiscono la valutazione degli script verrà trattato nei punti in cui i comportamenti di valutazione degli script variano.
Script caricati con l'elemento <script>
Il numero di attività inviate per valutare gli script ha in genere 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é è importante? Supponiamo che tu stia utilizzando un aggregatore per gestire gli script di produzione e che lo abbia configurato in modo da raggruppare in un unico script tutto ciò che serve alla tua pagina per funzionare. Se questo è il caso del tuo sito web, puoi prevedere che verrà inviata una singola attività per valutare lo script. È un dato negativo? Non necessariamente, a meno che lo script non sia enorme.
Puoi suddividere il lavoro di valutazione degli script evitando di caricare grandi blocchi di codice JavaScript e caricare più script singoli e più piccoli utilizzando elementi <script>
aggiuntivi.
Anche se dovresti sempre cercare di caricare la minore quantità di codice JavaScript possibile durante il caricamento della pagina, la suddivisione degli script garantisce che, invece di un'attività di grandi dimensioni che potrebbe bloccare il thread principale, si abbia un numero maggiore di attività più piccole che non bloccheranno il thread principale o almeno meno di quello con cui hai iniziato.
Puoi considerare la suddivisione delle attività per la valutazione dello script come un po' simile al rilascio durante i callback degli eventi eseguiti durante un'interazione. Tuttavia, con la valutazione dello script, il meccanismo di cessione suddivide il codice JavaScript caricato in più script più piccoli, anziché in un numero minore 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 dello sviluppatore, ad esempio non dover trasformare il codice per l'utilizzo in produzione, in particolare se utilizzato in combinazione con import maps. Tuttavia, il caricamento degli script in questo modo pianifica le attività che differiscono da browser a browser.
Browser basati su Chromium
In 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 vedi quando non utilizzi type=module
. Ad esempio, verrà eseguita un'attività per ogni script del modulo che coinvolge l'attività etichettata come Compila modulo.
Una volta compilati i moduli, qualsiasi codice eseguito successivamente attiverà l'attività etichettata come Valuta modulo.
Il risultato, almeno in Chrome e nei browser correlati, è che i passaggi di compilazione vengono suddivisi quando si utilizzano i moduli ES. Si tratta di un vantaggio evidente in termini di gestione di attività lunghe; tuttavia, il lavoro di valutazione del modulo che ne deriva comporta comunque un costo inevitabile. Anche se dovresti cercare di pubblicare 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à rigida, che consente potenziali ottimizzazioni da parte dei motori JavaScript che altrimenti non potrebbero essere effettuate in un contesto non rigido.
- Per impostazione predefinita, gli script caricati utilizzando
type=module
vengono trattati come se fossero differiti. Per modificare questo comportamento, è possibile utilizzare l'attributoasync
negli script caricati contype=module
.
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, potresti caricare un singolo modulo di primo livello composto da sole istruzioni import
statiche in altri moduli e ogni modulo caricato sarà sottoposto a una richiesta di rete e un'attività separate per valutarlo.
Script caricati con import()
dinamico
import()
dinamico è un altro metodo per caricare gli script. A differenza delle istruzioni import
statiche che devono trovarsi nella parte superiore di un modulo ES, una chiamata import()
dinamica può comparire in qualsiasi punto di uno script per caricare un frammento di JavaScript on demand. Questa tecnica è chiamata splitting del codice.
La pubblicità dinamica import()
presenta due vantaggi per il miglioramento dell'INP:
- I moduli il cui caricamento viene posticipato riducono la contesa del thread principale durante l'avvio riducendo la quantità di JavaScript caricata in quel momento. In questo modo, il thread principale viene liberato e può essere più reattivo alle interazioni degli utenti.
- Quando vengono eseguite chiamate
import()
dinamiche, ogni chiamata separa efficacemente la compilazione e la valutazione di ogni modulo in una propria attività. Ovviamente, un elementoimport()
dinamico che carica un modulo di grandi dimensioni avvia un'attività di valutazione degli script piuttosto di grandi dimensioni e può interferire con la capacità del thread principale di rispondere all'input dell'utente se l'interazione si verifica contemporaneamente alla chiamata dinamicaimport()
. Pertanto, è ancora molto importante caricare il minor codice JavaScript possibile.
Le chiamate import()
dinamiche si comportano in modo simile in tutti i principali motori dei browser: le attività di valutazione dello script che ne risultano saranno uguali alla quantità di moduli importati dinamicamente.
Script caricati in un worker web
I worker web sono un caso d'uso speciale di JavaScript. I worker web vengono registrati nel thread principale e il codice all'interno del worker viene eseguito nel proprio thread. Ciò è estremamente utile nel senso che, mentre il codice che registra il worker web viene eseguito sul thread principale, il codice all'interno del web worker non lo fa. Questo riduce la congestione del thread principale e può contribuire a mantenere il thread principale più reattivo alle interazioni degli utenti.
Oltre a ridurre il lavoro del thread principale, i propri worker web possono caricare script esterni da utilizzare nel contesto del worker tramite istruzioni importScripts
o import
statiche nei browser che supportano i worker di modulo. Il risultato è che qualsiasi script richiesto da un worker web viene valutato dal thread principale.
Scelte e considerazioni
Sebbene suddividere gli script in file separati più piccoli aiuti a limitare le attività lunghe rispetto al caricamento di un numero inferiore di file molto più grandi, è importante tenere conto di alcuni aspetti quando si decide come suddividere gli script.
Efficienza di compressione
La compressione è un fattore da considerare per suddividere 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 di compressione consenta di ridurre il più possibile i tempi di caricamento degli script, è un po' un compito di bilanciamento per garantire che vengano suddivisi in blocchi sufficientemente più piccoli da facilitare una migliore interattività durante l'avvio.
I bundler sono strumenti ideali per gestire le dimensioni di output degli script di cui dipende il tuo sito web:
- Per quanto riguarda webpack, il relativo plug-in
SplitChunksPlugin
può essere utile. Consulta la documentazione diSplitChunksPlugin
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 bundler, così come webpack, suddividono automaticamente l'asset importato dinamicamente in un file dedicato, evitando così dimensioni iniziali del bundle più grandi.
Annullamento convalida cache
L'invalidazione della cache ha un ruolo fondamentale nella velocità di caricamento di una pagina durante le visite ripetute. Quando pubblichi bundle di script monolitici di grandi dimensioni, hai uno svantaggio per quanto riguarda la memorizzazione nella cache del browser. Questo accade perché, quando aggiorni il codice proprietario, ad esempio aggiornando i pacchetti o implementando correzioni di bug, l'intero bundle viene invalidato e deve essere scaricato di nuovo.
Se dividi 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 acquisiscano più script dalla cache del browser anziché dalla rete. Ciò si traduce in un caricamento pagina più rapido.
Moduli nidificati e prestazioni di caricamento
Se carichi i moduli ES in produzione e li carichi con l'attributo type=module
, devi sapere in che modo l'annidamento dei moduli può influire sul tempo di avvio. Per nidificazione dei moduli si intende 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 moduli ES non sono raggruppati, il codice precedente genera una catena di richieste di rete: quando viene richiesto a.js
da un elemento <script>
, viene inviata un'altra richiesta di rete per b.js
, che a sua volta coinvolge un'altra richiesta per c.js
. Un modo per evitare che ciò accada è usare un bundler, ma assicurati di configurarlo in modo da suddividere gli script in modo da distribuire il lavoro di valutazione degli script.
Se non vuoi utilizzare un bundler, un altro modo per aggirare le chiamate di moduli nidificati è utilizzare l'suggerimento di risorsa 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 ardua. L'approccio dipende dai requisiti e dai vincoli del tuo sito web. Tuttavia, suddividendo gli script, distribuisci il lavoro di valutazione degli script su numerose attività più piccole e, di conseguenza, offri 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'attributotype=module
, evita di caricare script molto grandi, in quanto attiveranno attività di valutazione degli script che richiedono molte risorse e bloccano il thread principale. Suddividi gli script in più elementi<script>
per suddividere il lavoro. - L'utilizzo dell'attributo
type=module
per caricare i moduli ES in modo nativo nel browser avvia le singole attività per la valutazione per ogni script del modulo separato. - Riduci le dimensioni dei bundle iniziali utilizzando le chiamate
import()
dinamiche. Questo funziona anche nei bundler, in quanto questi ultimi trattano ogni modulo importato dinamicamente come un "punto di separazione", con il risultato che viene generato uno script separato per ogni modulo importato dinamicamente. - Assicurati di valutare i compromessi, come l'efficienza della compressione e l'invalidazione della cache. Gli script più grandi si comprimono meglio, ma è più probabile che richiedano un lavoro di valutazione degli script più costoso in meno attività e che comportino l'invalidazione della cache del browser, con una conseguente efficienza complessiva della cache inferiore.
- Se utilizzi i moduli ES in modo nativo senza il bundling, utilizza l'indicazione della risorsa
modulepreload
per ottimizzarne il caricamento durante l'avvio. - Come sempre, carica il meno JavaScript possibile.
Sicuramente è un equilibrio delicato, ma suddividendo gli script e riducendo i payload iniziali con import()
dinamici, puoi ottenere un migliore rendimento all'avvio e gestire meglio le interazioni degli utenti durante questo periodo cruciale. In questo modo, dovresti ottenere un punteggio migliore per la metrica INP, offrendo così un'esperienza utente migliore.