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

Brendan Kenny
Brendan Kenny

Publié le 6 mars 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • 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 contrôles de formulaire intégrés peuvent sembler cassés aux utilisateurs, comme si la page était figée, sans parler des composants personnalisés plus complexes.

scheduler.yield() est un moyen de céder la place au thread principal, ce qui permet au navigateur d'exécuter les tâches en attente de haute priorité, puis de reprendre l'exécution là où elle s'était arrêtée. Cela permet à une page de rester plus réactive 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 au niveau de l'expression await scheduler.yield() et cède la place au thread principal, ce qui permet de fractionner 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énement.

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 yield est planifiée pour s'exécuter avant toute autre tâche similaire mise en 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 fractionner les tâches, mais ces continuations s'exécutent généralement après toutes les nouvelles tâches déjà mises en file d'attente, ce qui peut entraîner de longs délais entre la cession au thread principal et l'exécution de leur travail.

Continuité priorisée après la génération

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énement exécute les tâches en termes de priorités explicites, mais les priorités relatives sont toujours là, comme un rappel requestIdleCallback s'exécutant après tous les rappels setTimeout mis en file d'attente, ou un écouteur d'événement d'entrée déclenché s'exécutant généralement avant une tâche mise en file d'attente avec setTimeout(callback, 0).

La planification des tâches par ordre de priorité rend cela plus explicite, ce qui permet de déterminer plus facilement quelle tâche s'exécutera avant une autre. Elle permet également d'ajuster les priorités pour modifier cet ordre d'exécution, si nécessaire.

Comme indiqué, l'exécution continue d'une fonction après le rendement avec scheduler.yield() est prioritaire par rapport au démarrage d'autres tâches. Le concept directeur est que la continuation d'une tâche doit s'exécuter en premier, avant de passer à d'autres tâches. Si la tâche est un code bien structuré qui cède périodiquement la main pour que le navigateur puisse effectuer d'autres tâches importantes (comme répondre aux saisies de l'utilisateur), elle ne doit pas être pénalisée pour cette cession 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 sont côte à côte, mais sur une page réelle, ils peuvent être appelés à des endroits complètement différents. Par exemple, un script propriétaire et un script tiers peuvent configurer indépendamment le travail à exécuter. Il peut également s'agir de deux tâches provenant de composants distincts qui sont déclenchées en profondeur dans le planificateur de votre framework.

Voici à quoi cela pourrait ressembler dans les outils de développement :

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

myJob est signalé comme une tâche longue, ce qui empêche le navigateur d'effectuer d'autres actions pendant son exécution. En partant du principe qu'il s'agit 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);
}

Étant donné que myJobPart2 devait s'exécuter avec setTimeout dans myJob, mais que cette planification s'exécute après que someoneElsesJob a déjà été planifié, voici à quoi ressemblera 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 deuxième est une longue tâche qui exécute "someoneElsesJob" et 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 maintenant la deuxième partie de myJob ne s'exécute 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 la place au thread principal pour s'assurer que la page restait réactive aux saisies de l'utilisateur, pas pour abandonner complètement le thread principal. Si someoneElsesJob est particulièrement lent ou si de nombreuses autres tâches ont également été planifiées en plus de someoneElsesJob, il peut s'écouler beaucoup de temps avant que la deuxième 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 poursuite de toute fonction l'invoquant dans une file d'attente de priorité légèrement supérieure à celle du démarrage de toute autre tâche similaire. 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 désormais comme suit :

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

Le navigateur a toujours la possibilité d'ê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. Par conséquent, myJob est terminée avant le début de someoneElsesJob. Cela correspond beaucoup plus à l'attente de céder la place au thread principal pour maintenir la réactivité, et non de l'abandonner complètement.

Héritage de priorité

Dans le cadre de l'API Prioritized Task Scheduling plus vaste, scheduler.yield() se combine bien avec les priorités explicites disponibles dans scheduler.postTask(). Sans priorité définie explicitement, un scheduler.yield() dans un rappel scheduler.postTask() agira pratiquement de la même manière que dans l'exemple précédent.

Toutefois, si une priorité est définie, par exemple 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é plus élevée que les autres tâches 'background' (afin d'obtenir la continuation prioritaire attendue avant toute tâche 'background' en attente), mais avec une priorité 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 'background' scheduler.postTask() (ou avec requestIdleCallback), la continuation après un scheduler.yield() 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 attendez d'un rendement dans une tâche à faible priorité.

Utiliser l'API

Pour l'instant, scheduler.yield() n'est disponible que dans les navigateurs basés sur Chromium. Pour l'utiliser, vous devez donc détecter les fonctionnalités et revenir à une méthode secondaire de génération 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 à céder à l'aide de setTimeout() et à accepter la perte d'une continuation prioritaire, ou même à ne pas céder dans les navigateurs non compatibles si cela 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 la vérification du type et la prise en charge de l'IDE si vous détectez la fonctionnalité 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 des tâches et scheduler.postTask(), consultez les documents scheduler.yield() et Planification des tâches prioritaires sur MDN.

Pour en savoir plus sur les tâches longues, leur impact sur l'expérience utilisateur et les mesures à prendre, consultez Optimiser les tâches longues.