Utiliser scheduler.yield() pour diviser les tâches longues

Publié le 6 mars 2025

Browser Support

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

Source

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:

Deux tâches affichées dans le panneau des performances des outils pour les développeurs Chrome Il est indiqué que les deux tâches sont longues, la fonction "myJob" occupant l'intégralité de l'exécution de la première tâche et "someoneElsesJob" l'intégralité de la deuxième tâche.

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:

Trois tâches affichées dans le panneau "Performances" des outils pour les développeurs Chrome La première exécute la fonction "myJobPart1", la seconde est une tâche longue qui exécute "someoneElsesJob", et enfin la troisième exécute "myJobPart2".

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:

Deux tâches affichées dans le panneau des performances des outils pour les développeurs Chrome Il est indiqué que les deux tâches sont longues, la fonction "myJob" occupant l'intégralité de l'exécution de la première tâche et "someoneElsesJob" l'intégralité de la deuxième tâche.

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.