Utilizza scheduler.yield() per suddividere le attività lunghe

Data di pubblicazione: 6 marzo 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: not supported.
  • Safari: not supported.

Source

Una pagina sembra lenta e non risponde quando le attività lunghe mantengono occupato il thread principale, impedendogli di svolgere altri lavori importanti, come rispondere all'input dell'utente. Di conseguenza, anche i controlli dei moduli integrati possono sembrare non funzionanti per gli utenti, come se la pagina fosse bloccata, per non parlare dei componenti personalizzati più complessi.

scheduler.yield() è un modo per cedere il controllo al thread principale, consentendo al browser di eseguire qualsiasi attività in attesa di alta priorità, per poi continuare l'esecuzione da dove si era interrotto. In questo modo, una pagina rimane più reattiva e, di conseguenza, contribuisce a migliorare l'Interaction to Next Paint (INP).

scheduler.yield offre un'API ergonomica che fa esattamente quello che dice: l'esecuzione della funzione chiamata viene interrotta all'espressione await scheduler.yield() e cede il controllo al thread principale, suddividendo l'attività. L'esecuzione del resto della funzione, chiamata continuazione della funzione, verrà pianificata per l'esecuzione in una nuova attività di loop di eventi.

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

Il vantaggio specifico di scheduler.yield è che la continuazione dopo il rendimento è pianificata per essere eseguita prima dell'esecuzione di altre attività simili inserite in coda dalla pagina. Dà la priorità al proseguimento di un'attività rispetto all'avvio di nuove attività.

Funzioni come setTimeout o scheduler.postTask possono essere utilizzate anche per suddividere le attività, ma queste continue vengono eseguite in genere dopo le nuove attività già in coda, con il rischio di lunghi ritardi tra il passaggio al thread principale e il completamento del lavoro.

Continuità con priorità dopo il rendimento

scheduler.yield fa parte dell'API Prioritized Task Scheduling. In qualità di sviluppatori web, in genere non parliamo dell'ordine in cui il loop di eventi esegue le attività in termini di priorità esplicite, ma le priorità relative sono sempre presenti, ad esempio un callback requestIdleCallback eseguito dopo eventuali callback setTimeout in coda o un gestore di eventi di input attivato in genere prima di un'attività in coda con setTimeout(callback, 0).

La programmazione delle attività con priorità lo rende più esplicito, semplificando la comprensione di quale attività verrà eseguita prima di un'altra e consentendo di modificare le priorità per cambiare l'ordine di esecuzione, se necessario.

Come accennato, l'esecuzione continua di una funzione dopo il rendimento con scheduler.yield() ha una priorità più alta rispetto all'avvio di altre attività. Il concetto guida è che la continuazione di un'attività deve essere eseguita prima di passare ad altre attività. Se l'attività è un codice di buon comportamento che restituisce periodicamente il controllo in modo che il browser possa fare altre cose importanti (ad esempio rispondere agli input dell'utente), non deve essere penalizzato per aver restituito il controllo assegnandogli la priorità dopo altre attività simili.

Ecco un esempio: due funzioni in coda per l'esecuzione in attività diverse utilizzando setTimeout.

setTimeout(myJob);
setTimeout(someoneElsesJob);

In questo caso, le due chiamate setTimeout sono una accanto all'altra, ma in una pagina reale potrebbero essere chiamate in punti completamente diversi, ad esempio uno script proprietario e uno di terze parti che configurano in modo indipendente il lavoro da eseguire oppure potrebbero essere due attività di componenti separati attivate in profondità nello scheduler del framework.

Ecco come potrebbe funzionare in DevTools:

Due attività mostrate nel riquadro Rendimento di Chrome DevTools. Entrambe sono indicate come attività lunghe, con la funzione "myJob" che occupa l'intera esecuzione della prima attività e "someoneElsesJob" che occupa l'intera seconda attività.

myJob viene segnalata come attività di lunga durata, impedendo al browser di eseguire altre operazioni durante l'esecuzione. Supponendo che provenga da uno script proprietario, possiamo suddividerlo:

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

Poiché l'esecuzione di myJobPart2 con setTimeout è stata pianificata per il giorno myJob, ma questa pianificazione viene eseguita dopo che è già stata pianificata l'esecuzione di someoneElsesJob, ecco come sarà l'esecuzione:

Tre attività mostrate nel riquadro Rendimento di Chrome DevTools. La prima esegue la funzione "myJobPart1", la seconda è un'attività lunga che esegue "someoneElsesJob" e infine la terza esegue "myJobPart2".

Abbiamo suddiviso l'attività con setTimeout in modo che il browser possa essere reattivo durante la metà di myJob, ma ora la seconda parte di myJob viene eseguita solo al termine di someoneElsesJob.

In alcuni casi, potrebbe andare bene, ma in genere non è ottimale. myJob cedeva il controllo al thread principale per assicurarsi che la pagina potesse rimanere reattiva agli input dell'utente, non per cedere del tutto il thread principale. Se someoneElsesJob è particolarmente lento o se sono stati pianificati molti altri job oltre a someoneElsesJob, potrebbe trascorrere molto tempo prima che venga eseguita la seconda metà di someoneElsesJob.myJob Probabilmente non era questa l'intenzione dello sviluppatore quando ha aggiunto setTimeout a myJob.

Inserisci scheduler.yield(), che inserisce la continuazione di qualsiasi funzione che la richiama in una coda con una priorità leggermente superiore rispetto all'avvio di altre attività simili. Se myJob viene modificato per utilizzarlo:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

Ora l'esecuzione ha il seguente aspetto:

Due attività mostrate nel riquadro Rendimento di Chrome DevTools. Entrambe sono indicate come attività lunghe, con la funzione "myJob" che occupa l'intera esecuzione della prima attività e "someoneElsesJob" che occupa l'intera seconda attività.

Il browser ha ancora la possibilità di essere reattivo, ma ora la continuazione dell'attività myJob ha la priorità rispetto all'avvio della nuova attività someoneElsesJob, quindi myJob viene completata prima dell'inizio di someoneElsesJob. Questo è molto più vicino all'aspettativa di cedere al thread principale per mantenere la reattività, senza rinunciare completamente al thread principale.

Eredità della priorità

Nell'ambito dell'API Prioritized Task Scheduling più grande, scheduler.yield() si combina bene con le priorità esplicite disponibili in scheduler.postTask(). Se non viene impostata una priorità esplicita, un scheduler.yield() all'interno di un callback scheduler.postTask() si comporta in modo sostanzialmente simile all'esempio precedente.

Tuttavia, se è impostata una priorità, ad esempio una priorità 'background' bassa:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

La continuazione verrà pianificata con una priorità superiore a quella delle altre attività 'background', ottenendo la continuazione con priorità prevista prima di qualsiasi lavoro 'background' in attesa, ma comunque una priorità inferiore rispetto ad altre attività predefinite o ad alta priorità.'background'

Ciò significa che se programmi un lavoro con priorità bassa con un 'background' scheduler.postTask() (o con requestIdleCallback), la continuazione dopo un scheduler.yield() all'interno attenderà anche il completamento della maggior parte delle altre attività e l'esecuzione del thread principale inattivo, che è esattamente ciò che vuoi ottenere dal rendimento in un job con priorità bassa.

Come utilizzare l'API

Per il momento, scheduler.yield() è disponibile solo nei browser basati su Chromium, quindi per utilizzarlo devi rilevare le funzionalità e utilizzare un metodo di riserva per il rendimento per gli altri browser.

scheduler-polyfill è un piccolo polyfill per scheduler.postTask e scheduler.yield che internamente utilizza una combinazione di metodi per emulare gran parte della potenza delle API di pianificazione in altri browser (anche se l'eredità della priorità scheduler.yield() non è supportata).

Per chi vuole evitare un polyfill, un metodo è utilizzare setTimeout() e accettare la perdita di una continuazione con priorità oppure non eseguire il cedimento nei browser non supportati, se non è accettabile. Per saperne di più, consulta la documentazione di scheduler.yield() in Ottimizza le attività lunghe.

I tipi wicg-task-scheduling possono essere utilizzati anche per eseguire il controllo del tipo e usufruire del supporto dell'IDE se stai rilevando la funzionalità scheduler.yield() e aggiungi un valore predefinito.

Scopri di più

Per ulteriori informazioni sull'API e su come interagisce con le priorità delle attività e scheduler.postTask(), consulta la documentazione di scheduler.yield() e Prioritized Task Scheduling (Pianificazione delle attività con priorità) su MDN.

Per scoprire di più sulle attività lunghe, su come influiscono sull'esperienza utente e su cosa fare, leggi l'articolo sull'ottimizzazione delle attività lunghe.