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. Cette année seulement, nous avons 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 cession

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 pour être terminée. Une fois la tâche terminée, le contrôle est rendu au thread principal, ce qui lui permet de traiter la tâche suivante de la file d'attente.

En dehors des cas extrêmes où une tâche ne se termine jamais (par exemple, une boucle infinie), 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 sont une source de mauvaise réactivité des pages, car elles retardent la capacité du navigateur à répondre aux entrées utilisateur. Plus les tâches longues se produisent souvent et plus elles durent, plus les utilisateurs risquent d'avoir l'impression que la page est lente ou même qu'elle est complètement défectueuse.

Toutefois, le fait que votre code déclenche 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'utiliser le thread principal plus rapidement qu'elles ne le feraient si elles devaient attendre la fin des tâches longues.

Illustration de la façon dont la division d'une tâche peut améliorer la réactivité des entrées. En haut, une tâche longue empêche l'exécution d'un gestionnaire d'événements jusqu'à ce qu'elle soit terminée. En bas, la tâche segmentée permet au gestionnaire d'événements de s'exécuter plus tôt qu'il ne l'aurait fait autrement.
Visualisation de la cession du contrôle au thread principal. En haut, le rendu ne se produit que lorsqu'une tâche est exécutée jusqu'à son terme, ce qui signifie que les tâches peuvent prendre plus de temps à s'exécuter avant de rendre 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. Les interactions utilisateur peuvent ainsi s'exécuter plus tôt, ce qui améliore la réactivité des 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 deviez tout faire avant de répondre à l'entrée utilisateur ou à d'autres tâches qui pourraient également être importantes." Il s'agit d'un outil précieux dans la boîte à outils d'un développeur qui peut contribuer grandement à améliorer l'expérience utilisateur.

Problème lié aux stratégies de rendement actuelles

Une méthode courante pour obtenir un résultat 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 s'arrête de lui-même, vous dites "divisons cette grande tâche en petits morceaux".

Toutefois, le rendement avec setTimeout entraîne un effet secondaire potentiellement indésirable: le travail qui vient après le point de rendement est placé à la fin de la file d'attente des tâches. Les tâches planifiées par les interactions utilisateur seront toujours placées en tête de la file d'attente, comme elles le devraient. Toutefois, le travail restant que vous souhaitiez effectuer après avoir explicitement cédé peut être encore retardé par d'autres tâches provenant de sources concurrentes qui ont été placées dans la file d'attente avant elles.

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 se compose de quelques boutons sur lesquels vous pouvez cliquer et d'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 supérieur intitulé Exécuter des tâches régulièrement. Les tâches bloquantes seront alors planifiées pour s'exécuter régulièrement. Lorsque vous cliquez sur ce bouton, plusieurs messages Exécuté la tâche de blocage avec setInterval s'affichent dans le journal des tâches.
  2. Cliquez ensuite sur le bouton Exécuter la boucle, avec 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

Cette sortie illustre le comportement de fin de la file d'attente de tâches qui se produit lors de l'abandon avec setTimeout. La boucle qui s'exécute traite cinq éléments et renvoie setTimeout après le traitement de chacun d'eux.

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 des tâches à un intervalle donné. Le comportement de "fin de file d'attente de tâches" qui s'applique lorsque vous cédez 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 cédé.

Selon votre application, ce résultat peut être souhaitable ou non. Toutefois, dans de nombreux cas, c'est ce comportement qui peut expliquer la réticence des développeurs à abandonner le contrôle du thread principal si facilement. La cession est utile, car les interactions utilisateur peuvent s'exécuter plus tôt, mais elle permet également à d'autres tâches non liées aux interactions utilisateur d'avoir du temps sur le thread principal. C'est un vrai problème, mais scheduler.yield peut vous aider à le résoudre.

Saisissez scheduler.yield.

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

Notez que l'abandon 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 expiration de 0 spécifiée. 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 plan de la file d'attente. Cela signifie que le travail que vous souhaitiez reprendre immédiatement après avoir cédé la priorité ne sera pas relégué au second plan par les tâches provenant d'autres sources (à l'exception notable des interactions utilisateur).

scheduler.yield est une fonction qui cède au thread principal et renvoie un Promise lorsqu'elle est appelée. Vous pouvez donc 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 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 supérieur intitulé Exécuter des tâches régulièrement.
  5. Enfin, cliquez sur le bouton Exécuter la boucle, avec scheduler.yield à chaque itération.

Le résultat de 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 un résultat à l'aide de setTimeout, vous pouvez constater que la boucle, même si elle génère un résultat après chaque itération, n'envoie pas le travail restant à l'arrière de la file d'attente, mais à l'avant. Vous bénéficiez ainsi du meilleur des deux mondes: vous pouvez céder pour améliorer la réactivité des entrées sur votre site Web, mais aussi vous assurer que le travail que vous souhaitiez terminer après la cession n'est pas retardé.

Faites le test !

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 localement, saisissez chrome://flags dans la barre d'adresse de Chrome, puis sélectionnez Enable (Activer) dans le menu déroulant de la section Experimental Web Platform Features (Fonctionnalités expérimentales de la plate-forme Web). scheduler.yield (et toutes les autres fonctionnalités expérimentales) ne sera disponible que dans votre instance de Chrome.
  2. Si vous souhaitez activer scheduler.yield pour de véritables utilisateurs Chromium sur une origine accessible au public, vous devez vous inscrire à la phase d'évaluation de l'origine scheduler.yield. Vous pouvez ainsi tester en toute sécurité les fonctionnalités proposées pendant une période donnée. L'équipe Chrome obtient ainsi des insights précieux sur la façon dont ces fonctionnalités sont utilisées sur le terrain. Pour en savoir plus sur le fonctionnement des essais d'origine, consultez ce guide.

La façon dont vous utilisez scheduler.yield (tout en prenant en charge 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 être en mesure de définir des tâches et des priorités en termes de rendement.
  3. Vous souhaitez pouvoir annuler ou réorganiser des tâches via la classe TaskController proposée par l'API scheduler.postTask.

Si ce n'est pas votre cas, le polyfill n'est peut-être pas fait pour vous. Dans ce cas, vous pouvez créer votre propre solution de remplacement de plusieurs façons. La première approche utilise scheduler.yield s'il est disponible, mais utilise setTimeout s'il ne l'est pas:

// 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 ne génèrent pas de comportement de "première de la file d'attente". 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 de planification. Il devrait permettre aux développeurs d'améliorer plus facilement la réactivité que 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 issue de Unsplash, par Jonathan Allison.