Publié le 6 mars 2025
Une page semble lente et ne répond pas lorsque des tâches longues occupent le thread principal, l'empêchant d'effectuer d'autres tâches importantes, comme répondre aux entrées utilisateur. Par conséquent, même les commandes de formulaire intégrées peuvent sembler défectueuses aux utilisateurs (comme si la page était figée), sans parler des composants personnalisés plus complexes.
scheduler.yield()
permet de céder au thread principal, ce qui permet au navigateur d'exécuter toute tâche en attente de priorité élevée, puis de poursuivre l'exécution là où il s'était arrêté. Cela permet de maintenir la réactivité d'une page et, par conséquent, d'améliorer l'Interaction to Next Paint (INP).
scheduler.yield
propose une API ergonomique qui fait exactement ce qu'elle dit: l'exécution de la fonction dans laquelle elle est appelée s'interrompt à l'expression await scheduler.yield()
et cède la place au thread principal, ce qui divise la tâche. L'exécution du reste de la fonction (appelée continuation de la fonction) sera planifiée pour s'exécuter dans une nouvelle tâche de boucle d'événements.
async function respondToUserClick() {
giveImmediateFeedback();
await scheduler.yield(); // Yield to the main thread.
slowerComputation();
}
L'avantage spécifique de scheduler.yield
est que la continuation après le rendement est planifiée pour s'exécuter avant l'exécution d'autres tâches similaires qui ont été placées dans la file d'attente par la page. Il donne la priorité à la poursuite d'une tâche plutôt qu'au démarrage de nouvelles tâches.
Des fonctions telles que setTimeout
ou scheduler.postTask
peuvent également être utilisées pour diviser les tâches, mais ces continuations s'exécutent généralement après les nouvelles tâches déjà mises en file d'attente, ce qui peut entraîner de longs délais entre le transfert au thread principal et l'achèvement de la tâche.
Poursuites prioritaires après cession
scheduler.yield
fait partie de l'API de planification des tâches prioritaires. En tant que développeurs Web, nous ne parlons généralement pas de l'ordre dans lequel la boucle d'événements exécute les tâches en termes de priorités explicites, mais les priorités relatives sont toujours présentes, comme un rappel requestIdleCallback
exécuté après tous les rappels setTimeout
mis en file d'attente ou un écouteur d'événement d'entrée déclenché qui s'exécute généralement avant une tâche mise en file d'attente avec setTimeout(callback, 0)
.
La planification des tâches prioritaires rend cela plus explicite, ce qui permet de déterminer plus facilement quelle tâche s'exécutera avant une autre et d'ajuster les priorités pour modifier cet ordre d'exécution, si nécessaire.
Comme indiqué, la poursuite de l'exécution d'une fonction après un retour avec scheduler.yield()
est prioritaire par rapport au démarrage d'autres tâches. Le concept directeur est que la poursuite d'une tâche doit s'exécuter en premier, avant de passer à d'autres tâches. Si la tâche est un code respectueux qui génère périodiquement des résultats afin que le navigateur puisse effectuer d'autres tâches importantes (comme répondre à la saisie utilisateur), elle ne doit pas être pénalisée pour avoir généré des résultats en étant priorisée après d'autres tâches similaires.
Voici un exemple: deux fonctions mises en file d'attente pour s'exécuter dans différentes tâches à l'aide de setTimeout
.
setTimeout(myJob);
setTimeout(someoneElsesJob);
Dans ce cas, les deux appels setTimeout
se trouvent juste à côté l'un de l'autre, mais dans une page réelle, ils peuvent être appelés à des endroits complètement différents, comme un script propriétaire et un script tiers configurant indépendamment le travail à exécuter, ou il peut s'agir de deux tâches de composants distincts déclenchées dans le planificateur de votre framework.
Voici à quoi cela pourrait ressembler dans DevTools:
myJob
est signalée comme une tâche longue, ce qui empêche le navigateur d'effectuer toute autre tâche pendant son exécution. En supposant qu'il s'agisse d'un script propriétaire, nous pouvons le décomposer:
function myJob() {
// Run part 1.
myJobPart1();
// Yield with setTimeout() to break up long task, then run part2.
setTimeout(myJobPart2, 0);
}
Comme myJobPart2
devait s'exécuter avec setTimeout
dans myJob
, mais que cette planification s'exécute après la planification de someoneElsesJob
, voici comment se présente l'exécution:
Nous avons divisé la tâche avec setTimeout
afin que le navigateur puisse être réactif au milieu de myJob
, mais la deuxième partie de myJob
ne s'exécute désormais qu'après la fin de someoneElsesJob
.
Dans certains cas, cela peut être acceptable, mais ce n'est généralement pas optimal. myJob
cédait au thread principal pour s'assurer que la page pouvait rester réactive aux entrées utilisateur, et non pour abandonner complètement le thread principal. Si someoneElsesJob
est particulièrement lent ou si de nombreuses autres tâches en plus de someoneElsesJob
ont été planifiées, il peut s'écouler un long moment avant que la seconde moitié de myJob
ne soit exécutée. Ce n'était probablement pas l'intention du développeur lorsqu'il a ajouté setTimeout
à myJob
.
Saisissez scheduler.yield()
, ce qui place la continuation de toute fonction l'appelant dans une file d'attente de priorité légèrement supérieure à celle de démarrage d'autres tâches similaires. Si myJob
est modifié pour l'utiliser:
async function myJob() {
// Run part 1.
myJobPart1();
// Yield with scheduler.yield() to break up long task, then run part2.
await scheduler.yield();
myJobPart2();
}
L'exécution se présente maintenant comme suit:
Le navigateur peut toujours être réactif, mais la poursuite de la tâche myJob
est désormais prioritaire par rapport au démarrage de la nouvelle tâche someoneElsesJob
. myJob
est donc terminée avant le début de someoneElsesJob
. Cela correspond beaucoup plus à l'attente de céder au thread principal pour maintenir la réactivité, sans abandonner complètement le thread principal.
Héritage de priorité
Dans le cadre de l'API de planification des tâches prioritaires plus vaste, scheduler.yield()
se compose bien avec les priorités explicites disponibles dans scheduler.postTask()
. Sans priorité explicitement définie, un scheduler.yield()
dans un rappel scheduler.postTask()
se comportera essentiellement de la même manière que dans l'exemple précédent.
Toutefois, si une priorité est définie, par exemple en utilisant une priorité 'background'
faible:
async function lowPriorityJob() {
part1();
await scheduler.yield();
part2();
}
scheduler.postTask(lowPriorityJob, {priority: 'background'});
La continuation sera planifiée avec une priorité supérieure à celle des autres tâches 'background'
(la continuation prioritaire attendue sera obtenue avant toute tâche 'background'
en attente), mais elle restera inférieure à celle des autres tâches par défaut ou à priorité élevée. Il s'agit toujours d'une tâche 'background'
.
Cela signifie que si vous planifiez une tâche à faible priorité avec un scheduler.postTask()
'background'
(ou avec requestIdleCallback
), la continuation après un scheduler.yield()
à l'intérieur attendra également que la plupart des autres tâches soient terminées et que le thread principal soit inactif pour s'exécuter, ce qui est exactement ce que vous voulez obtenir d'un rendement dans une tâche à faible priorité.
Utiliser l'API
Pour le moment, scheduler.yield()
n'est disponible que dans les navigateurs basés sur Chromium. Pour l'utiliser, vous devez donc effectuer une détection de fonctionnalités et utiliser une autre méthode de cession pour les autres navigateurs.
scheduler-polyfill
est un petit polyfill pour scheduler.postTask
et scheduler.yield
qui utilise en interne une combinaison de méthodes pour émuler une grande partie de la puissance des API de planification dans d'autres navigateurs (bien que l'héritage de priorité scheduler.yield()
ne soit pas pris en charge).
Pour ceux qui souhaitent éviter un polyfill, une méthode consiste à utiliser setTimeout()
et à accepter la perte d'une continuation prioritaire, voire à ne pas céder dans les navigateurs non compatibles si ce n'est pas acceptable. Pour en savoir plus, consultez la documentation sur scheduler.yield()
dans Optimiser les tâches longues.
Les types wicg-task-scheduling
peuvent également être utilisés pour obtenir une vérification de type et une compatibilité avec l'IDE si vous effectuez la détection de fonctionnalités scheduler.yield()
et ajoutez vous-même un remplacement.
En savoir plus
Pour en savoir plus sur l'API et son interaction avec les priorités de tâche et scheduler.postTask()
, consultez les documents scheduler.yield()
et Planification de tâches prioritaires sur MDN.
Pour en savoir plus sur les tâches longues, leur impact sur l'expérience utilisateur et ce que vous pouvez faire pour les optimiser, consultez cet article.