Présentation de la phase d'évaluation de scheduler.yield

Créer des sites Web qui réagissent rapidement aux commentaires des utilisateurs a été l'un des aspects les plus difficiles des performances Web, un défi auquel l'équipe Chrome a travaillé dur pour aider les développeurs Web à y répondre. Rien que cette année, il a été annoncé que la métrique Interaction to Next Paint (INP) passerait de l'état expérimental à l'état "En attente". Il devrait remplacer le First Input Delay (FID) en tant que Core Web Vitals en mars 2024.

Pour continuer à proposer de nouvelles API permettant aux développeurs Web de rendre leurs sites Web aussi dynamiques que possible, l'équipe Chrome lance actuellement une phase d'évaluation pour scheduler.yield à partir de la version 115 de Chrome. scheduler.yield est un nouvel ajout proposé à l'API du programmeur. Il offre un moyen plus simple et plus efficace de restituer le contrôle au thread principal que les méthodes traditionnellement utilisées.

Sur le rendement

JavaScript utilise le modèle d'exécution jusqu'à la fin pour gérer les tâches. Cela signifie que lorsqu'une tâche s'exécute sur le thread principal, elle s'exécute aussi longtemps que nécessaire. Une fois une tâche terminée, le contrôle est renvoyé au thread principal, ce qui permet au thread principal de traiter la tâche suivante dans la file d'attente.

Hormis les cas extrêmes où une tâche ne se termine jamais, comme une boucle infinie, par exemple, le rendement est un aspect inévitable de la logique de planification des tâches de JavaScript. Cela se produira, ce n'est qu'une question de quand, et le plus tôt sera le mieux. Lorsque l'exécution d'une tâche prend trop de temps (plus de 50 millisecondes pour être exacte), elle est considérée comme de longues tâches.

Les longues tâches ralentissent la réactivité des pages, car elles retardent la réponse du navigateur à l'entrée utilisateur. Plus les tâches longues sont fréquentes (et plus leur durée d'exécution est longue), plus les utilisateurs sont susceptibles de donner l'impression que la page est lente ou même d'avoir l'impression qu'elle ne fonctionne pas.

Toutefois, ce n'est pas parce que votre code lance une tâche dans le navigateur que vous devez attendre qu'elle soit terminée pour redonner le contrôle au thread principal. Vous pouvez améliorer la réactivité aux entrées utilisateur sur une page en renvoyant explicitement des éléments dans une tâche, ce qui interrompt la tâche pour qu'elle soit terminée lors de la prochaine opportunité disponible. Cela permet aux autres tâches de gagner du temps sur le thread principal plus rapidement que si elles devaient attendre l'achèvement de longues tâches.

Représentation de la façon dont la division d'une tâche peut faciliter une meilleure réactivité aux entrées. En haut, une longue tâche empêche un gestionnaire d'événements de s'exécuter jusqu'à la fin de la tâche. En bas, la tâche fragmentée permet au gestionnaire d'événements de s'exécuter plus tôt qu'il ne l'aurait fait autrement.
Visualisation de la restitution du contrôle au thread principal. En haut, le rendement ne se produit qu'après l'exécution d'une tâche, ce qui signifie que les tâches peuvent prendre plus de temps avant de rendre le contrôle au thread principal. En bas, le rendement est effectué explicitement, en divisant une longue tâche en plusieurs tâches plus petites. Cela permet aux interactions utilisateur de s'exécuter plus tôt, ce qui améliore la réactivité aux entrées et l'INP.

Lorsque vous cédez explicitement, vous dites au navigateur "Hé, je comprends que le travail que je m'apprête à effectuer peut prendre un certain temps, et je ne veux pas que vous ayez à faire tout ce travail avant de répondre aux entrées utilisateur ou à d'autres tâches qui pourraient également être importantes". Il s'agit d'un outil précieux pour les développeurs, qui peut contribuer grandement à améliorer l'expérience utilisateur.

Le problème avec les stratégies de rendement actuelles

Une méthode courante pour générer utilise setTimeout avec une valeur de délai avant expiration de 0. Cela fonctionne, car le rappel transmis à setTimeout déplacera le travail restant vers une tâche distincte qui sera mise en file d'attente pour une exécution ultérieure. Plutôt que d'attendre que le navigateur cède de lui-même le résultat, vous pouvez dire qu'il faut diviser cette grosse portion de travail en plus petites parties.

Cependant, le rendement avec setTimeout entraîne un effet secondaire potentiellement indésirable: la tâche qui vient après le point de rendement sera placée à la fin de la file d'attente de tâches. Les tâches planifiées par les interactions des utilisateurs apparaissent toujours en tête de la file d'attente comme elles le devraient, mais le travail restant que vous vouliez effectuer après avoir explicitement cédé la liste pourrait être davantage retardé par d'autres tâches provenant de sources concurrentes qui étaient en file d'attente.

Pour voir cela en action, essayez cette démonstration de Glitch ou faites-en une expérience dans la version intégrée ci-dessous. La démonstration se compose de quelques boutons sur lesquels vous pouvez cliquer et d'une case en dessous qui consigne l'exécution des tâches. Une fois sur cette page, effectuez les actions suivantes:

  1. Cliquez sur le bouton du haut intitulé Exécuter des tâches à intervalles réguliers pour programmer l'exécution des tâches bloquantes de façon régulière. Lorsque vous cliquez sur ce bouton, le journal des tâches contient plusieurs messages indiquant A exécuté une tâche bloquante avec setInterval.
  2. Cliquez ensuite sur le bouton Exécuter une boucle, avec setTimeout à chaque itération.

Vous remarquerez que la zone située en bas de la démo se présente comme suit:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

Ce résultat illustre le comportement "fin de la file d'attente de tâches" qui se produit lorsque l'utilisateur abandonne avec setTimeout. La boucle qui exécute cinq éléments traite cinq éléments et génère setTimeout une fois que chacun d'eux a été traité.

Cela illustre un problème courant sur le Web: il n'est pas rare qu'un script (en particulier un script tiers) enregistre une fonction de minuteur qui exécute une tâche à un intervalle donné. Le comportement de "fin de la file d'attente de tâches" lié au rendement avec setTimeout signifie que le travail provenant d'autres sources de tâches peut être mis en file d'attente avant le travail restant que la boucle doit effectuer après avoir généré.

En fonction de votre application, ce résultat peut être souhaitable ou non. Toutefois, dans de nombreux cas, ce comportement est la raison pour laquelle les développeurs peuvent se sentir réticents à renoncer si facilement au contrôle du thread principal. Le rendement est bon, car les interactions utilisateur ont la possibilité de s'exécuter plus tôt, mais cela permet également de gagner du temps sur le thread principal avec d'autres tâches d'interaction non utilisateur. C'est un vrai problème, mais scheduler.yield peut vous aider à le résoudre.

Saisissez scheduler.yield

scheduler.yield est disponible derrière un drapeau en tant que fonctionnalité expérimentale de plate-forme Web depuis la version 115 de Chrome. Vous vous demandez peut-être pourquoi ai-je besoin d'une fonction spéciale pour générer alors que setTimeout le fait déjà ?

Il convient de noter que le rendement n'était pas un objectif de conception de setTimeout, mais plutôt un effet secondaire intéressant lors de la planification d'un rappel à exécuter ultérieurement, même avec une valeur de délai avant 0 spécifiée. Cependant, il est important de se rappeler que le rendement avec setTimeout envoie le travail restant à l'arrière-plan de la file d'attente de tâches. Par défaut, scheduler.yield envoie les tâches restantes au premier de la file d'attente. Cela signifie que le travail que vous souhaitiez reprendre immédiatement après l'obtention ne sera pas interrompu par des tâches provenant d'autres sources (à l'exception notable des interactions utilisateur).

scheduler.yield est une fonction qui revient au thread principal et renvoie un Promise lorsqu'elle est appelée. Cela signifie que vous pouvez await dans une fonction async:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

Pour voir scheduler.yield en action, procédez comme suit:

  1. Accédez à chrome://flags.
  2. Activez le test des fonctionnalités expérimentales de la plate-forme Web. Vous devrez peut-être redémarrer Chrome après avoir effectué cette opération.
  3. Accédez à la page de démonstration ou utilisez la version intégrée sous cette liste.
  4. Cliquez sur le bouton Exécuter des tâches régulièrement.
  5. Enfin, cliquez sur le bouton Exécuter une boucle, avec scheduler.yield à chaque itération.

Le résultat dans la zone située en bas de la page se présente comme suit:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

Contrairement à la démonstration qui utilise setTimeout, vous pouvez voir que la boucle, même si elle cède après chaque itération, n'envoie pas le travail restant à la fin de la file d'attente, mais plutôt au début. Vous bénéficiez ainsi du meilleur des deux mondes: vous pouvez améliorer la réactivité aux entrées sur votre site Web, tout en vous assurant que le travail que vous vouliez terminer après avoir généré ne sera pas retardé.

Essayez vous aussi !

Si scheduler.yield vous intéresse et que vous voulez l'essayer, vous pouvez le faire de deux manières à partir de la version 115 de Chrome:

  1. Si vous souhaitez tester scheduler.yield en local, saisissez chrome://flags dans la barre d'adresse de Chrome, puis sélectionnez Activer dans le menu déroulant de la section Fonctionnalités expérimentales de la plate-forme Web. scheduler.yield (et toutes les autres fonctionnalités expérimentales) sera ainsi disponible uniquement dans votre instance de Chrome.
  2. Si vous souhaitez activer scheduler.yield pour les utilisateurs de Chromium réels sur une origine accessible au public, vous devez vous inscrire à la phase d'évaluation de scheduler.yield. Vous pouvez ainsi tester en toute sécurité les fonctionnalités proposées pendant une période donnée et fournir à l'équipe Chrome des informations précieuses sur la façon dont elles sont utilisées sur le terrain. Pour en savoir plus sur le fonctionnement des phases d'évaluation, consultez ce guide.

Votre utilisation de scheduler.yield (tout en étant compatible avec les navigateurs qui ne l'implémentent pas) dépend de vos objectifs. Vous pouvez utiliser le polyfill officiel. Le polyfill est utile si les conditions suivantes s'appliquent à votre situation:

  1. Vous utilisez déjà scheduler.postTask dans votre application pour planifier des tâches.
  2. Vous devez pouvoir définir des priorités pour les tâches et le rendement.
  3. Vous souhaitez pouvoir annuler ou redéfinir la priorité des tâches via la classe TaskController proposée par l'API scheduler.postTask.

Si cela ne correspond pas à votre situation, le polyfill n'est peut-être pas fait pour vous. Dans ce cas, vous pouvez effectuer la création de remplacement de différentes manières. La première approche utilise scheduler.yield s'il est disponible, mais utilise setTimeout dans le cas contraire:

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

Cela peut fonctionner, mais comme vous pouvez le deviner, les navigateurs qui ne sont pas compatibles avec scheduler.yield généreront sans comportement "face à la file d'attente". Si cela signifie que vous préférez ne pas céder du tout, vous pouvez essayer une autre approche qui utilise scheduler.yield s'il est disponible, mais qui ne donnera pas du tout dans le cas contraire:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield est un ajout intéressant à l'API Scheduler, qui devrait permettre aux développeurs d'améliorer plus facilement la réactivité qu'avec les stratégies de rendement actuelles. Si scheduler.yield vous semble être une API utile, veuillez participer à nos recherches pour nous aider à l'améliorer et nous faire part de vos commentaires sur la façon dont nous pourrions l'améliorer.

Image principale de Unsplash, de Jonathan Allison.