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

Créer des sites Web qui réagissent rapidement aux commentaires des utilisateurs a constitué l'un des aspects les plus difficiles des performances Web, et l'équipe Chrome s'est efforcée d'aider les développeurs Web à répondre à leurs attentes. 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 est sur le point de remplacer le FID (First Input Delay) en tant que métrique Core Web Vitals en mars 2024.

Afin de continuer à proposer de nouvelles API qui aident les développeurs Web à rendre leurs sites Web aussi rapides que possible, l'équipe Chrome mène actuellement une phase d'évaluation pour scheduler.yield à partir de la version 115 de Chrome. scheduler.yield est une nouvelle proposition proposée pour l'API Scheduler. Elle offre un moyen plus simple et plus efficace de céder le contrôle au thread principal que les méthodes traditionnellement utilisées.

Lors de la génération

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 réduit au thread principal, ce qui lui permet de traiter la tâche suivante de la file d'attente.

Hormis dans 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 va arriver, mais ce n'est qu'une question de quand, et le plus tôt sera préférable. 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 une tâche longue.

Les tâches longues peuvent nuire à la réactivité des pages, car elles retardent la capacité du navigateur à répondre à l'entrée utilisateur. Plus les tâches sont longues et plus le temps d'exécution est long, plus il est probable que les utilisateurs aient l'impression que la page est lente, voire qu'elle ne fonctionne pas du tout.

Toutefois, le fait que votre code lance une tâche dans le navigateur ne signifie pas que vous devez attendre que celle-ci soit terminée pour que le contrôle soit rendu au thread principal. Vous pouvez améliorer la réactivité aux entrées utilisateur sur une page en renvoyant explicitement dans une tâche, ce qui la divise pour qu'elle soit terminée à la prochaine opportunité disponible. Cela permet aux autres tâches d'obtenir du temps sur le thread principal plus tôt que si elles devaient attendre la fin de longues tâches.

<ph type="x-smartling-placeholder">
</ph> Représentation de la façon dont la division d&#39;une tâche peut améliorer la réactivité aux entrées. En haut, une longue tâche empêche un gestionnaire d&#39;événements de s&#39;exécuter jusqu&#39;à ce que la tâche soit terminée. En bas, la tâche fragmentée permet au gestionnaire d&#39;événements de s&#39;exécuter plus tôt qu&#39;il ne l&#39;aurait fait autrement. <ph type="x-smartling-placeholder">
</ph> Visualisation permettant de céder le 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 redonner le contrôle au thread principal. Dans la partie inférieure, 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 rapidement, ce qui améliore la réactivité aux entrées et l'INP.

Lorsque vous cédez explicitement, vous dites au navigateur "Je comprends que le travail que je suis sur le point d'effectuer peut prendre un certain temps, et je ne veux pas que vous ayez à effectuer tout cette tâche avant de répondre à l'entrée utilisateur ou à d'autres tâches qui peuvent également être importantes". C'est un outil précieux qui se trouve dans la boîte à outils du développeur et qui peut grandement contribuer à améliorer l'expérience utilisateur.

Le problème des stratégies de rendement actuelles

Une méthode courante d'affichage 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 abandonne le processus, vous dites qu'il faut diviser cette grande partie du travail en parties plus petites.

Cependant, le rendement avec setTimeout a un effet secondaire potentiellement indésirable: le travail qui vient après le point de rendement reviendra à l'arrière de la file d'attente de tâches. Les tâches planifiées par des interactions utilisateur seront toujours placées en tête de la file d'attente comme elles le devraient, mais le travail restant que vous vouliez effectuer après avoir généré explicitement le rendement pourrait finir par être encore plus retardé par d'autres tâches provenant de sources concurrentes placées en file d'attente devant elle.

Pour voir comment cela fonctionne, essayez cette démonstration de Glitch ou testez-le dans la version intégrée ci-dessous. La démonstration comprend quelques boutons sur lesquels vous pouvez cliquer et une zone en dessous qui consigne les tâches exécutées. Une fois sur la page, effectuez les actions suivantes:

  1. Cliquez sur le bouton du haut intitulé Exécuter des tâches régulièrement pour programmer l'exécution des tâches bloquantes de temps en temps. Lorsque vous cliquez sur ce bouton, le journal des tâches est renseigné avec plusieurs messages indiquant A exécuté bloquant une tâche avec setInterval.
  2. Cliquez ensuite sur le bouton Run loop, en générant setTimeout à chaque itération.

Dans la zone située en bas de la démo, vous pouvez voir un texte qui ressemble à ceci:

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 la "fin de la file d'attente de tâches" comportement qui se produit lors de la génération avec setTimeout. La boucle qui exécute cinq éléments et renvoie une valeur 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 s'exécute à intervalles réguliers. La "fin de la file d'attente de tâches" Le comportement associé 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 le rendement.

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 sont réticents à abandonner si facilement le contrôle du thread principal. Le rendement est bon, car les interactions utilisateur peuvent s'exécuter plus tôt, mais cela permet également à d'autres tâches d'interaction non utilisateur de gagner du temps sur le thread principal également. C'est un problème réel, mais scheduler.yield peut vous aider à le résoudre.

Saisissez scheduler.yield.

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

Notez que le rendement n'était pas un objectif de conception de setTimeout, mais plutôt un effet secondaire intéressant pour programmer l'exécution d'un rappel à un moment ultérieur, même avec une valeur de délai avant expiration spécifiée sur 0. Toutefois, il est plus important de se rappeler que la génération de données avec setTimeout envoie le travail restant à l'arrière de la file d'attente de tâches. Par défaut, scheduler.yield envoie le travail restant au premier de la file d'attente. Cela signifie que le travail que vous vouliez reprendre immédiatement après avoir cédé n'occupera pas un siège arrière aux tâches provenant d'autres sources (à l'exception notable des interactions utilisateur).

scheduler.yield est une fonction qui renvoie au thread principal et renvoie Promise lorsqu'elle est appelée. Cela signifie que vous pouvez l'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 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 du haut intitulé Exécuter les tâches régulièrement.
  5. Pour finir, cliquez sur le bouton Run loop, en générant scheduler.yield à chaque itération.

Le résultat affiché dans la zone 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 génère le résultat à l'aide de setTimeout, vous pouvez voir que la boucle, même si elle génère après chaque itération, n'envoie pas le travail restant à l'arrière de la file d'attente, mais plutôt en tête. Vous bénéficiez ainsi du meilleur des deux mondes: vous pouvez augmenter le rendement pour améliorer la réactivité aux entrées sur votre site Web, tout en veillant à ce que le travail que vous vouliez terminer après le rendement ne soit 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 toute autre fonctionnalité expérimentale) ne sera ainsi disponible que dans votre instance de Chrome.
  2. Si vous souhaitez activer scheduler.yield pour les utilisateurs réels de Chromium sur une origine accessible publiquement, vous devez vous inscrire à la phase d'évaluation de scheduler.yield. Cela vous permet de tester les fonctionnalités proposées en toute sécurité pendant une période donnée et donne à l'équipe Chrome des informations précieuses sur la façon dont ces fonctionnalités sont utilisées sur le terrain. Pour en savoir plus sur le fonctionnement des phases d'évaluation, consultez ce guide.

La manière dont vous utilisez scheduler.yield, tout en continuant à accepter les navigateurs qui ne l'implémentent pas, dépend de vos objectifs. Vous pouvez utiliser le polyfill officiel. Le polyfill est utile dans les cas suivants:

  1. Vous utilisez déjà scheduler.postTask dans votre application pour planifier des tâches.
  2. Vous devez être en mesure de définir les tâches et les priorités associées.
  3. Vous souhaitez pouvoir annuler des tâches ou définir leurs priorités à l'aide de 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 le rollback de votre propre création de remplacement de plusieurs 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 renverront sans "en tête de file d'attente" comportemental. Si cela signifie que vous préférez ne pas rendement du tout, vous pouvez essayer une autre approche qui utilise scheduler.yield s'il est disponible, mais qui ne générera aucun rendement 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. Nous espérons qu'il permettra 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 à notre étude 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 tirée de Unsplash, par Jonathan Allison.