Introduzione alla prova dell'origine scheduler.yield

La creazione di siti web che rispondono rapidamente all'input utente è stata una delle sfide più difficili in termini di prestazioni web, una sfida che il team di Chrome si è impegnato a risolvere per aiutare gli sviluppatori web. Proprio quest'anno è stato annunciato che la metrica Interaction to Next Paint (INP) sarebbe passata dallo stato sperimentale a quello in attesa. Ora è pronta a sostituire First Input Delay (FID) come Core Web Vital a marzo 2024.

Nel continuo impegno per fornire nuove API che aiutino gli sviluppatori web a rendere i loro siti web il più veloci possibile, il team di Chrome sta attualmente eseguendo una prova dell'origine per scheduler.yield a partire dalla versione 115 di Chrome. scheduler.yield è una nuova aggiunta proposta all'API Scheduler che consente di restituire il controllo al thread principale in modo più semplice e migliore rispetto a i metodi tradizionalmente utilizzati.

Informazioni sulla restituzione del controllo

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 al completamento. Al termine di un'attività, il controllo viene restituito al thread principale, che può quindi elaborare l'attività successiva nella coda.

A parte i casi estremi in cui un'attività non termina mai, ad esempio un loop infinito, la restituzione del controllo è un aspetto inevitabile della logica di pianificazione delle attività di JavaScript. Succederà, è solo una questione di quando e prima è meglio di dopo. Quando le attività richiedono troppo tempo per essere eseguite, per la precisione più di 50 millisecondi, vengono considerate attività lunghe.

Le attività lunghe sono una fonte di scarsa reattività della pagina, perché ritardano la capacità del browser di rispondere all'input utente. Più spesso si verificano attività lunghe e più a lungo vengono eseguite, maggiore è la probabilità che gli utenti abbiano l'impressione che la pagina sia lenta o addirittura che sia completamente danneggiata.

Tuttavia, solo perché il codice avvia 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 su una pagina restituendo esplicitamente il controllo in un'attività, che viene suddivisa per essere completata alla prima opportunità disponibile. In questo modo, le altre attività possono essere eseguite sul thread principale prima di dover attendere il completamento delle attività lunghe.

Una rappresentazione di come la suddivisione di un'attività possa facilitare una migliore reattività dell'input. In alto, un'attività lunga impedisce l'esecuzione di un gestore di eventi finché l'attività non è terminata. In basso, l'attività suddivisa consente al gestore di eventi di essere eseguito prima del previsto.
Una visualizzazione della restituzione del controllo al thread principale. Nella parte superiore, la restituzione del controllo avviene solo dopo il completamento di un'attività, il che significa che le attività possono richiedere più tempo per essere completate prima di restituire il controllo al thread principale. Nella parte inferiore, la restituzione del controllo viene eseguita in modo esplicito, suddividendo un'attività lunga in diverse attività più piccole. In questo modo, le interazioni utente possono essere eseguite prima, il che migliora la reattività all'input e INP.

Quando restituisci esplicitamente il controllo, stai dicendo al browser: "Ok, so che il lavoro che sto per fare potrebbe richiedere un po' di tempo e non voglio che tu debba fare tutto questo lavoro prima di rispondere all'input utente o ad altre attività che potrebbero essere importanti". È uno strumento prezioso nella cassetta degli attrezzi di uno sviluppatore che può contribuire notevolmente a migliorare l'esperienza utente.

Il problema con le attuali strategie di restituzione del controllo

Un metodo comune di restituzione del controllo 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 restituisca il controllo da solo, stai dicendo "suddividi questo grande blocco di lavoro in parti più piccole".

Tuttavia, la restituzione del controllo con setTimeout comporta un effetto collaterale potenzialmente indesiderabile: il lavoro che viene dopo il punto di restituzione del controllo verrà messo in coda alla fine della coda delle attività. Le attività pianificate dalle interazioni utente verranno comunque messe in coda all'inizio, come dovrebbero, ma il lavoro rimanente che volevi fare dopo aver restituito esplicitamente il controllo potrebbe essere ulteriormente ritardato da altre attività provenienti da fonti concorrenti che sono state messe in coda prima.

Per vedere questo in azione, prova questa demo di Codepen o sperimentala nella seguente versione incorporata. La demo è composta da alcuni pulsanti su cui puoi fare clic e da una casella sottostante che registra quando vengono eseguite le attività. Quando accedi alla pagina, esegui le seguenti azioni:

  1. Fai clic sul pulsante in alto con l'etichetta Esegui attività periodicamente, che pianificherà l'esecuzione periodica delle attività di blocco. Quando fai clic su questo pulsante, il log delle attività verrà compilato con diversi messaggi con l'etichetta Attività di blocco eseguita con setInterval.
  2. Poi, fai clic sul pulsante con l'etichetta Esegui loop, restituendo il controllo con setTimeout a ogni iterazione.

Noterai che la casella nella parte inferiore della demo conterrà un testo 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 dimostra il comportamento di "fine della coda di attività" che si verifica quando si restituisce il controllo con setTimeout. Il loop che esegue l'elaborazione di cinque elementi e restituisce il controllo con setTimeout dopo l'elaborazione di ciascuno.

Questo illustra un problema comune sul web: non è raro che uno script, in particolare uno script di terze parti, registri una funzione timer che esegue il lavoro a intervalli regolari. Il comportamento di "fine della coda di attività" che si verifica quando si restituisce il controllo con setTimeout significa che il lavoro proveniente da altre fonti di attività potrebbe essere messo in coda prima del lavoro rimanente che il loop deve eseguire dopo la restituzione del controllo.

A seconda dell'applicazione, questo potrebbe essere un risultato desiderabile o meno, 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 restituzione del controllo è utile perché le interazioni utente hanno la possibilità di essere eseguite prima, ma consente anche ad altri lavori non di interazione utente di essere eseguiti sul thread principale. È un vero problema, ma scheduler.yield può aiutarti a risolverlo.

Introduzione a scheduler.yield

scheduler.yield è disponibile dietro un flag come funzionalità sperimentale della piattaforma web dalla versione 115 di Chrome. Una domanda che potresti farti è: "Perché ho bisogno di una funzione speciale per restituire il controllo quando setTimeout lo fa già?"

È importante notare che la restituzione del controllo non era un obiettivo di progettazione di setTimeout, ma piuttosto un effetto collaterale positivo nella pianificazione di un callback da eseguire in un secondo momento, anche con un valore di timeout di 0 specificato. Tuttavia, la cosa più importante da ricordare è che la restituzione del controllo 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 la restituzione del controllo non verrà messo in secondo piano rispetto alle attività provenienti da altre fonti (con la notevole eccezione delle interazioni utente).

scheduler.yield è una funzione che restituisce il controllo al thread principale e restituisce una 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:

  1. Vai a chrome://flags.
  2. Attiva l'esperimento Funzionalità sperimentali della piattaforma web. Potresti dover riavviare Chrome dopo aver eseguito questa operazione.
  3. Vai a la pagina della demo o utilizza la seguente versione incorporata dopo questo elenco.
  4. Fai clic sul pulsante in alto con l'etichetta Esegui attività periodicamente.
  5. Infine, fai clic sul pulsante con l'etichetta Esegui loop, restituendo il controllo con scheduler.yield a ogni iterazione.

L'output nella casella nella parte inferiore della 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 il controllo utilizzando setTimeout, puoi notare che il loop, anche se restituisce il controllo dopo ogni iterazione, non invia il lavoro rimanente alla fine della coda, ma all'inizio. In questo modo, puoi ottenere il meglio da entrambi i mondi: puoi restituire il controllo per migliorare la reattività all'input sul tuo sito web, ma anche assicurarti che il lavoro che volevi completare dopo la restituzione del controllo 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:

  1. Se vuoi sperimentare scheduler.yield localmente, digita e inserisci chrome://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.
  2. Se vuoi attivare scheduler.yield per gli utenti reali di Chromium su un'origine accessibile pubblicamente, devi registrarti alla prova dell'origine di scheduler.yield. In questo modo, puoi sperimentare in sicurezza le funzionalità proposte per un determinato periodo di tempo e il team di Chrome riceve 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 continuando a supportare i browser che non lo implementano, dipende dai tuoi obiettivi. Puoi utilizzare il polyfill ufficiale. Il polyfill è utile se si verifica la seguente situazione:

  1. Stai già utilizzando scheduler.postTask nella tua applicazione per pianificare le attività.
  2. Vuoi poter impostare le priorità delle attività e della restituzione del controllo.
  3. Vuoi poter annullare o riassegnare la priorità alle attività utilizzando la classe TaskController offerta dall'API scheduler.postTask.

Se questa descrizione non si adatta alla tua situazione, il polyfill potrebbe non essere adatto a te. In questo caso, puoi creare il tuo fallback in due modi. Il primo approccio utilizza scheduler.yield se è disponibile, ma esegue il fallback a setTimeout se non lo è:

// 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 restituiranno il controllo senza il comportamento "inizio della coda". Se preferisci non restituire affatto il controllo, puoi provare un altro approccio che utilizza scheduler.yield se è disponibile, ma non restituisce affatto il controllo se non lo è:

// 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'aggiunta interessante all'API Scheduler che, si spera, renderà più facile per gli sviluppatori migliorare la reattività rispetto alle attuali strategie di restituzione del controllo. Se scheduler.yield ti sembra un'API utile, partecipa alla nostra ricerca per contribuire a migliorarla e fornisci feedback su come potrebbe essere ulteriormente migliorata.

Immagine promozionale di Unsplash, di Jonathan Allison.