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 troverai riguarda l'ottimizzazione delle interazioni stesse. Ad esempio, nella guida all'ottimizzazione delle attività lunghe vengono discusse tecniche come il rendimento con setTimeout
e altre. Queste tecniche sono utili perché consentono al thread principale di respirare evitando attività lunghe, il che può consentire più opportunità di interazioni e altre attività da eseguire prima, anziché dover attendere una singola attività lunga.
Tuttavia, che dire delle 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 utente durante il caricamento della pagina.
Che cos'è la valutazione dello 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 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 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 visualizzata 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
. 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, dove i comportamenti di valutazione degli script variano.
Script caricati con l'elemento <script>
Il numero di attività inviate per valutare gli script ha generalmente una relazione diretta con il 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 è 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 per 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 JavaScript e caricare più script singoli e più piccoli utilizzando elementi <script>
aggiuntivi.
Sebbene tu debba sempre cercare di caricare il meno JavaScript possibile durante il caricamento della pagina, la suddivisione degli script ti garantisce che, anziché una grande attività che potrebbe bloccare il thread principale, tu abbia un numero maggiore di attività più piccole che non bloccheranno affatto 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 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 dello sviluppatore, ad esempio non dover trasformare il codice per l'utilizzo in produzione, in particolare se utilizzato in combinazione con le mappe di importazione. Tuttavia, il caricamento degli script in questo modo pianifica attività che variano da un browser all'altro.
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.
L'effetto, 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à rigorosa, il che consente potenziali ottimizzazioni da parte degli engine JavaScript che altrimenti non potrebbero essere apportate in un contesto non rigoroso.
- 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 costituito solo da istruzioni statiche import
in altri moduli e ogni modulo caricato comporterà 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 in modo efficace la compilazione e la valutazione di ogni modulo in una propria attività. Ovviamente, unimport()
dinamico che carica un modulo molto grande avvierà 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 chiamataimport()
dinamica. 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. Questo è estremamente vantaggioso nel senso che, mentre il codice che registra il web worker viene eseguito nel thread principale, il codice all'interno del web worker non viene eseguito. 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 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 al di fuori del 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 contribuisca a ridurre al minimo i tempi di caricamento degli script, è necessario trovare il giusto equilibrio per assicurarsi di suddividere gli script in blocchi sufficientemente 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 le correzioni dei bug, l'intero bundle diventa non valido 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 complessivo della 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. Il nidificazione dei moduli si verifica 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 a.js
viene richiesto da un elemento <script>
, viene inviata un'altra richiesta di rete per b.js
, che 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 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 riepilogare, 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 avvierà singole attività di 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.