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 provochi attività lunghe durante il caricamento della pagina.

Per quanto riguarda l'ottimizzazione dell'interazioni con Next Paint (INP), la maggior parte dei consigli che riceverai è quella 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, in quanto consentono al thread principale di respirare evitando attività lunghe, il che può consentire di eseguire più rapidamente le interazioni e altre attività, anziché dover aspettare un'unica attività lunga.

E per quanto riguarda invece 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 vedremo 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 profilato un'applicazione che invia molto JavaScript, potresti aver visto attività lunghe in cui il colpevole è etichettato come Valuta script.

La valutazione degli script funziona come indicato nel profiler delle prestazioni di Chrome DevTools. Questo lavoro comporta 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 a causare un'attività lunga che impedisce al thread principale di svolgere altri lavori, incluse 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 prima analizzato per rilevare eventuali errori. Se l'analizzatore sintattico non trova errori, lo script viene compilato in bytecode e quindi può continuare con l'esecuzione.

Sebbene sia necessario, la valutazione dello script può essere problematico, in quanto gli utenti potrebbero provare a interagire con una pagina subito dopo il rendering iniziale. Tuttavia, il semplice fatto che una pagina sia stata rendering 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 vi sia alcuna garanzia che in questo momento possa avvenire un'interazione, dato che lo script responsabile potrebbe non essere stato ancora caricato, potrebbero esserci delle interazioni dipendenti da JavaScript che sono pronte oppure 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 che lo script sia un modulo caricato con type=module. Dal momento che i browser hanno la tendenza a gestire le cose in modo diverso, il modo in cui i principali motori dei browser gestiscono la valutazione degli script verrà toccato dove 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. È il caso dei browser basati su Chromium, Safari e Firefox.

Perché tenerne conto? Supponiamo che tu stia utilizzando un bundler per gestire i tuoi script di produzione e che lo abbia configurato in modo da raggruppare tutto ciò che deve essere eseguito dalla pagina in un unico script. Se questo è il caso del tuo sito web, puoi aspettarti che venga inviata un'unica 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 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.

Più attività che implicano la valutazione degli script, così come vengono visualizzate nel profiler delle prestazioni di Chrome DevTools. Poiché vengono caricati più script di piccole dimensioni anziché meno, le attività hanno meno probabilità di diventare 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 seguito della presenza di più elementi <script> nel codice HTML della pagina. È preferibile inviare agli utenti un unico pacchetto di script di grandi dimensioni, che è più probabile che blocchi il thread principale.

Puoi pensare di suddividere le attività per la valutazione degli script come un'azione in qualche modo simile alla restituzione durante i callback di eventi che vengono eseguiti durante un'interazione. Tuttavia, durante la valutazione degli script, il meccanismo di rendimento suddivide il codice JavaScript che carichi in più script più piccoli, invece di un numero inferiore di script più grandi di quelli che con maggiori probabilità bloccano 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 nell'elemento <script>. Questo approccio al caricamento degli script offre alcuni vantaggi per gli sviluppatori, come il fatto che non è necessario trasformare il codice per l'uso in produzione, soprattutto se utilizzato in combinazione con l'importazione delle mappe. 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 da esso derivati), il caricamento dei moduli ES utilizzando l'attributo type=module produce tipi di attività diversi rispetto a quelli che vedresti normalmente se non usi type=module. Ad esempio, verrà eseguita un'attività per ogni script del modulo che coinvolge un'attività etichettata come Compila modulo.

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

Una volta compilati i moduli, qualsiasi codice che viene eseguito al loro interno darà inizio all'attività etichettata come Valuta modulo.

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

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 chiaro vantaggio in termini di gestione di attività lunghe; tuttavia, il conseguente lavoro di valutazione del modulo comporta comunque dei costi inevitabili. Nonostante sia consigliabile distribuire 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.
  • Per impostazione predefinita, gli script caricati utilizzando type=module vengono trattati come se fossero stati posticipati. Per modificare questo comportamento, è possibile utilizzare l'attributo async negli script caricati con type=module.

Safari e Firefox

Quando i moduli vengono caricati in Safari e Firefox, ognuno 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 la dimensione dinamica import()

Un altro metodo per caricare gli script è dinamico import(). A differenza delle istruzioni import statiche, che devono trovarsi 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.

Lo strumento import() dinamico ha due vantaggi per migliorare l'INP:

  1. I moduli che vengono rinviati per essere caricati in un secondo momento riducono il conflitto del thread principale durante l'avvio, diminuendo la quantità di codice JavaScript caricato in quel momento. In questo modo il thread principale viene liberato e può essere più reattivo alle interazioni degli utenti.
  2. Quando vengono effettuate chiamate import() dinamiche, ciascuna chiamata separerà in modo efficace la compilazione e la valutazione di ciascun modulo dalla propria attività. Ovviamente, un elemento import() 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 dinamica import(). Pertanto, è comunque molto importante caricare la minore quantità di codice JavaScript possibile.

Le chiamate dinamiche import() si comportano in modo simile in tutti i principali motori dei browser: le attività di valutazione degli script che ne risultano corrisponderanno alla quantità di moduli importati in modo dinamico.

Script caricati in un worker web

I lavoratori web 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 sul 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, gli stessi web worker possono caricare script esterni da utilizzare nel contesto dei worker, tramite istruzioni importScripts o import statiche nei browser che supportano i lavoratori del modulo. Il risultato è che qualsiasi script richiesto da un worker web viene valutato dal thread principale.

Vantaggi e considerazioni

La suddivisione degli script in file separati e di dimensioni inferiori consente di limitare le attività più lunghe, anziché di caricare un numero inferiore di file molto più grandi, ma è importante prendere in considerazione alcuni aspetti quando si decide come suddividere gli script.

Efficienza di compressione

La compressione è un fattore importante per la suddivisione degli script. Quando gli script sono più piccoli, la compressione diventa leggermente meno efficiente. Gli script più grandi traggono maggiore vantaggio dalla compressione. Sebbene l'aumento dell'efficienza della 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 la dimensione di output degli script da cui dipende il tuo sito web:

  • Per quanto riguarda webpack, il suo plug-in SplitChunksPlugin può essere d'aiuto. Consulta la documentazione di SplitChunksPlugin per scoprire le opzioni che puoi impostare per gestire le dimensioni degli asset.
  • Per altri bundleer 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 in un proprio file, evitando così dimensioni iniziali maggiori.

Annullamento convalida cache

L'annullamento della convalida della cache gioca un ruolo importante nella velocità di caricamento di una pagina durante le visite ripetute. La distribuzione di grandi bundle di script monolitici comporta uno svantaggio per quanto riguarda la memorizzazione nella cache del browser. Questo perché quando aggiorni il codice proprietario (tramite l'aggiornamento dei pacchetti o la spedizione di correzioni di bug), l'intero bundle viene invalidato e deve essere scaricato di nuovo.

Suddividere gli script non solo scompone il lavoro di valutazione degli script in attività più piccole, ma aumenta 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 spedisci moduli ES in produzione e li carichi con l'attributo type=module, devi fare attenzione a come la nidificazione dei moduli può influire sui tempi 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 usare un bundler, un altro modo per aggirare le chiamate dei moduli nidificate è utilizzare il hint modulepreload resource, che precaricherà in anticipo i moduli ES per evitare catene di richieste di rete.

Conclusione

L'ottimizzazione della valutazione degli script nel browser è senza dubbio un'impresa complicata. L'approccio dipende dai requisiti e dai vincoli del tuo sito web. Tuttavia, la suddivisione degli script consente di distribuire il lavoro di valutazione degli script su numerose attività più piccole, consentendo al thread principale 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 molto grandi, poiché attiveranno attività di valutazione degli script che consumano molte risorse e bloccano il thread principale. Distribuite i vostri 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 avvia le singole attività per la valutazione per ogni script del modulo separato.
  • Riduci le dimensioni dei bundle iniziali utilizzando chiamate import() dinamiche. Questa operazione funziona anche nei bundler, in quanto i bundler trattano ogni modulo importato dinamicamente come un "punto di suddivisione", con la conseguente generazione di uno script separato per ciascun modulo importato in modo dinamico.
  • Assicurati di valutare i compromessi come l'efficienza di compressione e l'annullamento della convalida della cache. Gli script più grandi si comprimono meglio, ma hanno maggiori probabilità di comportare un lavoro di valutazione degli script più costoso con meno attività e comportano l'annullamento della convalida della cache del browser, con una conseguente riduzione complessiva dell'efficienza della memorizzazione nella cache.
  • Se utilizzi i moduli ES in modo nativo senza raggruppamento, usa il suggerimento delle risorse modulepreload per ottimizzarne il caricamento durante l'avvio.
  • Come sempre, fornisci meno codice JavaScript possibile.

Si tratta di un equilibrio di sicuro, ma suddividendo gli script e riducendo i payload iniziali con la import() dinamica, puoi ottenere migliori prestazioni all'avvio e soddisfare meglio le interazioni degli utenti durante il periodo di avvio cruciale. Questo dovrebbe aiutarti a ottenere un punteggio migliore nella metrica INP, offrendo così un'esperienza utente migliore.

Immagine hero da Unsplash, di Markus Spiske.