Data di pubblicazione: 6 marzo 2025
Una pagina risulta lenta e non reattiva quando le attività lunghe tengono occupato il thread principale, impedendogli di svolgere altre attività importanti, come rispondere all'input utente. Di conseguenza, anche i controlli dei moduli integrati possono apparire agli 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 lavoro in attesa ad alta priorità, per poi continuare l'esecuzione da dove era stata interrotta. 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 ciò che dice: l'esecuzione della funzione in cui viene chiamata si interrompe 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à del ciclo 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 l'esecuzione prima di qualsiasi altra attività simile messa in coda dalla pagina. Dà la priorità alla continuazione di un'attività rispetto all'avvio di nuove attività.
Anche funzioni come setTimeout
o scheduler.postTask
possono essere utilizzate per suddividere le attività, ma queste continuazioni 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.
Continuazioni con priorità dopo la cessione
scheduler.yield
fa parte dell'API Prioritized Task Scheduling. In qualità di sviluppatori web, in genere non parliamo dell'ordine in cui il ciclo di eventi esegue le attività in termini di priorità esplicite, ma le priorità relative sono sempre presenti, ad esempio una callback requestIdleCallback
viene eseguita dopo qualsiasi callback setTimeout
in coda oppure un listener di eventi di input attivato viene in genere eseguito prima di un'attività accodata con setTimeout(callback, 0)
.
La pianificazione delle attività con priorità rende tutto più esplicito, semplificando la determinazione dell'attività da eseguire prima di un'altra e consentendo di modificare le priorità per cambiare l'ordine di esecuzione, se necessario.
Come accennato, la continua esecuzione di una funzione dopo la generazione 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 ben funzionante che viene eseguito periodicamente in modo che il browser possa svolgere altre attività importanti (come rispondere all'input dell'utente), non deve essere penalizzata per la sua esecuzione periodica, ma deve essere considerata prioritaria rispetto ad altre attività simili.
Ecco un esempio: due funzioni, messe in coda per essere eseguite 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 posizioni completamente diverse, ad esempio uno script proprietario e uno script 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 apparire questo lavoro in DevTools:
myJob
è contrassegnata come attività lunga, impedendo al browser di svolgere 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
era pianificata con setTimeout
entro myJob
, ma questa pianificazione viene eseguita dopo che someoneElsesJob
è già stata pianificata, ecco come si svolgerà l'esecuzione:
Abbiamo suddiviso l'attività con setTimeout
in modo che il browser possa rispondere durante la metà di myJob
, ma ora la seconda parte di myJob
viene eseguita solo dopo il completamento di someoneElsesJob
.
In alcuni casi, questa soluzione può essere accettabile, ma di solito non è ottimale. myJob
stava cedendo il controllo al thread principale per assicurarsi che la pagina potesse rimanere reattiva all'input dell'utente, non per rinunciare completamente al thread principale. Nei casi in cui someoneElsesJob
è particolarmente lento o sono stati pianificati molti altri job oltre a someoneElsesJob
, potrebbe passare molto tempo prima che venga eseguita la seconda metà di myJob
. Probabilmente non era 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 di 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:
Il browser ha ancora la possibilità di rispondere, 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 il controllo al thread principale per mantenere la reattività, non di abbandonarlo completamente.
Ereditarietà della priorità
Nell'ambito dell'API Prioritized Task Scheduling più ampia, scheduler.yield()
si integra bene con le priorità esplicite disponibili in scheduler.postTask()
. Senza una priorità impostata in modo esplicito, un scheduler.yield()
all'interno di un callback scheduler.postTask()
si comporterà sostanzialmente come nell'esempio precedente.
Tuttavia, se viene impostata una priorità, ad esempio utilizzando 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 rispetto ad altre attività 'background'
, in modo da ottenere la continuazione con priorità prevista prima di qualsiasi lavoro 'background'
in attesa, ma comunque con una priorità inferiore rispetto ad altre attività predefinite o ad alta priorità; rimane un lavoro 'background'
.
Ciò significa che se pianifichi un lavoro a bassa priorità con un 'background'
scheduler.postTask()
(o con requestIdleCallback
), la continuazione dopo un scheduler.yield()
all'interno attenderà anche che la maggior parte delle altre attività siano completate e che il thread principale sia inattivo per essere eseguita, il che è esattamente ciò che vuoi dal rendimento in un job a bassa priorità.
Come utilizzare l'API
Per il momento, scheduler.yield()
è disponibile solo nei browser basati su Chromium, quindi per utilizzarlo dovrai rilevare le funzionalità e ripiegare su un modo secondario di cedere per gli altri browser.
scheduler-polyfill
è un piccolo polyfill per scheduler.postTask
e scheduler.yield
che utilizza internamente una combinazione di metodi per emulare gran parte della potenza delle API di pianificazione in altri browser (anche se l'ereditarietà della priorità scheduler.yield()
non è supportata).
Per chi vuole evitare un polyfill, un metodo consiste nell'utilizzare setTimeout()
e accettare la perdita di una continuazione con priorità o anche di non utilizzare yield nei browser non supportati, se non è accettabile. Per saperne di più, consulta la documentazione scheduler.yield()
in Ottimizzare le attività di lunga durata.
I tipi wicg-task-scheduling
possono essere utilizzati anche per ottenere il controllo dei tipi e il supporto dell'IDE se rilevi la funzionalità scheduler.yield()
e aggiungi un fallback.
Scopri di più
Per saperne di più sull'API e su come interagisce con le priorità delle attività e scheduler.postTask()
, consulta la documentazione scheduler.yield()
e Prioritized Task Scheduling su MDN.
Per saperne di più sulle attività di lunga durata, su come influiscono sull'esperienza utente e su cosa fare al riguardo, leggi l'articolo sull'ottimizzazione delle attività di lunga durata.