Introduzione alla prova dell'origine scheduler.yield

La creazione di siti web che rispondono rapidamente all'input degli utenti è uno degli aspetti più impegnativi 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 nel 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 (oltre i 50 millisecondi per essere precisi), 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 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à hanno tempo sul thread principale in anticipo rispetto al tempo necessario attendere il completamento di attività lunghe.

Un'immagine che mostra in che modo suddividere un'attività può facilitare una maggiore reattività all'input. In alto, un'attività lunga impedisce l'esecuzione di un gestore eventi fino al termine dell'attività. In basso, l'attività suddivisa in blocchi consente all'handler dell'evento di essere eseguito prima.
Una visualizzazione del trasferimento del controllo al thread principale. In alto, il rendimento si verifica solo dopo l'esecuzione completa di un'attività, il che significa che le attività possono richiedere più tempo per essere completate prima di restituire il controllo al thread principale. In basso, il rendimento è fatto esplicitamente, suddividendo un'attività lunga in diverse attività più piccole. In questo modo, le interazioni utente vengono eseguite prima, il che migliora la reattività all'input e l'INP.

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 delle 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 dopo il punto di rendimento verrà inserito in fondo alla coda delle attività. Le attività pianificate dalle interazioni degli utenti continueranno a essere posizionate in cima alla coda, come dovrebbero, ma il lavoro rimanente che volevi svolgere dopo aver ceduto in modo esplicito potrebbe finire per essere ulteriormente ritardato da altre attività di origini concorrenti che erano 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 viene visualizzata la pagina, esegui le seguenti azioni:

  1. Fai clic sul pulsante in alto con l'etichetta Esegui attività periodicamente, che pianificherà l'esecuzione del blocco delle attività di tanto in 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.
  2. Poi, fai clic sul pulsante Esegui il loop, restituendo setTimeout a ogni iterazione.

Noterai che nella parte inferiore della demo troverai qualcosa di simile a questo:

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 della tua applicazione, questo potrebbe essere o meno un risultato desiderabile, ma in molti casi, questo comportamento è il motivo per cui gli sviluppatori possono sentirsi riluttanti a rinunciare facilmente al 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 il rendimento non era un obiettivo di progettazione di setTimeout, ma un effetto collaterale positivo nella pianificazione di un callback da eseguire in un momento successivo, anche con un valore di timeout pari a 0 specificato. Tuttavia, è più importante ricordare che, cedendo con setTimeout, il lavoro rimanente viene spostato in back 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:

  1. Vai a chrome://flags.
  2. Attiva l'esperimento Funzionalità della piattaforma web sperimentali. Potresti dover riavviare Chrome dopo questa operazione.
  3. Vai alla pagina di demo o utilizza la versione incorporata sotto questo elenco.
  4. Fai clic sul pulsante in alto con l'etichetta Esegui attività periodicamente.
  5. 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 di 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:

  1. Se vuoi fare esperimenti con scheduler.yield localmente, digita 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 per la prova dell'origine scheduler.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 saperne di più 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:

  1. Utilizzi già scheduler.postTask nella tua applicazione per pianificare le attività.
  2. Vuoi poter impostare le attività e produrre le priorità.
  3. Vuoi poter annullare o ridefinire la priorità delle attività tramite la classe TaskController offerta dall'API scheduler.postTask.

Se la tua situazione non corrisponde a questa descrizione, il polyfill potrebbe non essere adatto a te. In questo caso, puoi eseguire il rollback autonomamente in un paio di 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.