La creazione di siti web che rispondano rapidamente all'input degli utenti è uno degli aspetti più complessi delle prestazioni web, un aspetto che il team di Chrome si impegna a soddisfare per aiutare gli sviluppatori web. Solo quest'anno è stato annunciato che la metrica Interaction to Next Paint (INP) passerà dallo stato sperimentale a quello in attesa. A marzo 2024 sostituirà il First Input Delay (FID) come metrica di Core Web Vital.
Nell'ambito del costante impegno per fornire nuove API che aiutino gli sviluppatori web a rendere i propri siti web il più rapidi possibile, il team di Chrome sta attualmente conducendo una prova dell'origine per scheduler.yield
a partire dalla versione 115 di Chrome. scheduler.yield
è una nuova aggiunta proposta all'API di pianificazione che consente un modo più semplice e migliore per restituire il controllo al thread principale rispetto ai metodi tradizionalmente utilizzati.
Al momento di cedere il passo
JavaScript utilizza il modello di esecuzione fino al completamento per gestire le attività. Ciò significa che, quando un'attività viene eseguita sul thread principale, viene eseguita per tutto il tempo necessario per il completamento. Al termine di un'attività, il controllo viene rilasciato al thread principale, che può elaborare l'attività successiva nella coda.
A parte i casi estremi in cui un'attività non termina mai, ad esempio un ciclo infinito, l'abbandono è un aspetto inevitabile della logica di pianificazione delle attività di JavaScript. Avverrà, è solo questione di quando e meglio prima che tardi. Quando l'esecuzione delle attività richiede troppo tempo (più di 50 millisecondi, per l'esattezza), queste sono considerate attività lunghe.
Le attività lunghe sono una fonte di scarsa reattività della pagina, perché ritardano la capacità del browser di rispondere all'input dell'utente. Più spesso si verificano attività lunghe e più a lungo vengono eseguite, più è probabile che gli utenti abbiano l'impressione che la pagina sia lenta o addirittura che sia del tutto inaccessibile.
Tuttavia, il fatto che il codice avvii un'attività nel browser non significa che devi attendere il completamento dell'attività prima che il controllo venga restituito al thread principale. Puoi migliorare la reattività all'input utente in una pagina cedendo esplicitamente in un'attività, che viene suddivisa per essere completata alla prima occasione disponibile. In questo modo, le altre attività possono ottenere tempo sul thread principale prima rispetto a quanto accadrebbe se dovessero attendere il completamento di attività lunghe.
Quando cedi esplicitamente, dici al browser: "sappiamo che il lavoro che sto per fare potrebbe richiedere un po' di tempo e non voglio che tu debba farlo tutto prima di rispondere all'input dell'utente o ad altre attività che potrebbero essere importanti". Si tratta di uno strumento prezioso nella cassetta degli attrezzi di uno sviluppatore che può contribuire in modo significativo a migliorare l'esperienza utente.
Il problema con le attuali strategie di rendimento
Un metodo comune per generare utilizza setTimeout
con un valore di timeout di 0
. Questo funziona perché il callback passato a setTimeout
sposterà il lavoro rimanente in un'attività separata che verrà messa in coda per l'esecuzione successiva. Anziché attendere che il browser si interrompa autonomamente, di' "suddividiamo questo grande blocco di lavoro in parti più piccole".
Tuttavia, l'abbandono con setTimeout
comporta un effetto collaterale potenzialmente indesiderato: il lavoro che segue il punto di rendimento verrà inserito in fondo alla coda delle attività. Le attività pianificate in base alle interazioni degli utenti verranno comunque inserite in prima fila nella coda, come previsto, ma il lavoro rimanente che volevi svolgere dopo aver ceduto esplicitamente potrebbe finire per essere ulteriormente ritardato da altre attività di origini concorrenti che sono state messe in coda prima.
Per vedere come funziona, prova questa demo di Glitch o esegui esperimenti nella versione incorporata di seguito. La demo è composta da alcuni pulsanti su cui puoi fare clic e da una casella sottostante che registra l'esecuzione delle attività. Quando accedi alla pagina, svolgi le seguenti azioni:
- Fai clic sul pulsante in alto etichettato Esegui attività periodicamente, che pianifica l'esecuzione delle attività di blocco ogni tanto. Quando fai clic su questo pulsante, nel log delle attività vengono visualizzati diversi messaggi con il messaggio È stata eseguita l'attività di blocco con
setInterval
. - Poi, fai clic sul pulsante Esegui il loop, restituendo
setTimeout
a ogni iterazione.
Nella casella in fondo alla demo viene visualizzato un messaggio simile al seguente:
Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Questo output mostra il comportamento "fine coda di attività" che si verifica quando si cede con setTimeout
. Il ciclo che viene eseguito elabora cinque elementi e restituisce setTimeout
dopo l'elaborazione di ciascuno.
Questo illustra un problema comune sul web: non è insolito che uno script, in particolare uno script di terze parti, registri una funzione timer che esegue un'operazione a un determinato intervallo. Il comportamento "fine coda di attività" che si verifica con l'abbandono con setTimeout
significa che il lavoro di altre origini attività potrebbe essere messo in coda prima del lavoro rimanente che il ciclo deve eseguire dopo l'abbandono.
A seconda dell'applicazione, questo può essere o meno un risultato auspicabile, ma in molti casi questo comportamento è il motivo per cui gli sviluppatori potrebbero essere riluttanti a cedere così facilmente il controllo del thread principale. La resa è utile perché le interazioni utente hanno la possibilità di essere eseguite prima, ma consente anche ad altri lavori di interazione non utente di ricevere tempo sul thread principale. Si tratta di un problema serio, ma scheduler.yield
può aiutarti a risolverlo.
Inserisci scheduler.yield
scheduler.yield
è disponibile dietro un flag come funzionalità sperimentale della piattaforma web dalla versione 115 di Chrome. Potresti chiederti: "Perché ho bisogno di una funzione speciale per generare un valore quando setTimeout
lo fa già?"
Vale la pena notare che l'abbandono non era un obiettivo di progettazione di setTimeout
, ma piuttosto un piacevole effetto collaterale della pianificazione di un callback da eseguire in un secondo momento, anche con un valore di timeout di 0
specificato. Tuttavia, è più importante ricordare che l'abbandono con setTimeout
invia il lavoro rimanente alla fine della coda di attività. Per impostazione predefinita, scheduler.yield
invia il lavoro rimanente all'inizio della coda. Ciò significa che il lavoro che volevi riprendere immediatamente dopo il rendimento non passerà in secondo piano rispetto alle attività di altre origini (con la notevole eccezione delle interazioni utente).
scheduler.yield
è una funzione che cede il controllo al thread principale e restituisce un Promise
quando viene chiamata. Ciò significa che puoi await
in una funzione async
:
async function yieldy () {
// Do some work...
// ...
// Yield!
await scheduler.yield();
// Do some more work...
// ...
}
Per vedere scheduler.yield
in azione:
- Vai a
chrome://flags
. - Attiva l'esperimento Funzionalità della piattaforma web sperimentali. Potresti dover riavviare Chrome dopo questa operazione.
- Vai alla pagina di demo o utilizza la versione incorporata sotto questo elenco.
- Fai clic sul pulsante in alto etichettato Esegui attività periodicamente.
- Infine, fai clic sul pulsante Esegui il loop, restituendo
scheduler.yield
a ogni iterazione.
L'output nella casella in fondo alla pagina sarà simile al seguente:
Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
A differenza della demo che restituisce utilizzando setTimeout
, puoi vedere che il loop, anche se restituisce dopo ogni iterazione, non invia il lavoro rimanente alla fine della coda, ma all'inizio. In questo modo puoi avere il meglio da entrambi i mondi: puoi eseguire il cedimento per migliorare la reattività degli input sul tuo sito web, ma anche assicurarti che il lavoro che volevi completare dopo il cedimento non venga ritardato.
Prova anche tu!
Se scheduler.yield
ti sembra interessante e vuoi provarlo, puoi farlo in due modi a partire dalla versione 115 di Chrome:
- Se vuoi fare esperimenti con
scheduler.yield
localmente, digitachrome://flags
nella barra degli indirizzi di Chrome e seleziona Attiva dal menu a discesa nella sezione Funzionalità sperimentali della piattaforma web. In questo modo,scheduler.yield
(e qualsiasi altra funzionalità sperimentale) sarà disponibile solo nella tua istanza di Chrome. - Se vuoi attivare
scheduler.yield
per gli utenti reali di Chromium su un'origine accessibile pubblicamente, devi registrarti per la prova dell'originescheduler.yield
. In questo modo, puoi sperimentare in sicurezza le funzionalità proposte per un determinato periodo di tempo e fornire al team di Chrome informazioni preziose su come vengono utilizzate queste funzionalità sul campo. Per ulteriori informazioni sul funzionamento delle prove dell'origine, leggi questa guida.
Il modo in cui utilizzi scheduler.yield
, pur supportando i browser che non lo implementano, dipende dai tuoi obiettivi. Puoi utilizzare il polyfill ufficiale. Il polyfill è utile se la tua situazione corrisponde a quanto segue:
- Utilizzi già
scheduler.postTask
nella tua applicazione per pianificare le attività. - Vuoi poter impostare le priorità delle attività e delle cessioni.
- Vuoi poter annullare o ridefinire la priorità delle attività tramite la classe
TaskController
offerta dall'APIscheduler.postTask
.
Se la tua situazione non corrisponde a questa descrizione, il polyfill potrebbe non essere adatto a te. In questo caso, puoi implementare il tuo piano di riserva in due modi. Il primo approccio utilizza scheduler.yield
se disponibile, ma passa a setTimeout
se non è disponibile:
// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to setTimeout:
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
Questo può funzionare, ma, come puoi immaginare, i browser che non supportano scheduler.yield
non avranno il comportamento "primo in coda". Se preferisci non generare alcun rendimento, puoi provare un altro approccio che utilizza scheduler.yield
se disponibile, ma non genera alcun rendimento se non è disponibile:
// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
// Use scheduler.yield if it exists:
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fall back to nothing:
return;
}
// Example usage:
async function doWork () {
// Do some work:
// ...
await yieldToMain();
// Do some other work:
// ...
}
scheduler.yield
è un'interessante aggiunta all'API di pianificazione, che dovrebbe consentire agli sviluppatori di migliorare la reattività rispetto alle attuali strategie di rendimento. Se ritieni che scheduler.yield
sia un'API utile, partecipa alla nostra ricerca per aiutarci a migliorarla e fornisci un feedback su come potrebbe essere ulteriormente migliorata.
Immagine hero di Unsplash, di Jonathan Allison.