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

Créer des sites Web qui répondent rapidement aux entrées utilisateur est l'un des aspects les plus difficiles des performances Web. L'équipe Chrome s'efforce de fournir aux développeurs Web les outils nécessaires pour y parvenir. 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. Elle est désormais prête à remplacer le First Input Delay (FID) en tant que métrique Core Web Vitals en mars 2024.

Dans le but de proposer de nouvelles API qui aident les développeurs Web à rendre leurs sites Web aussi rapides que possible, l'équipe Chrome effectue actuellement une phase d'évaluation de l'origine pour scheduler.yield à partir de la version 115 de Chrome. scheduler.yield est une nouvelle fonctionnalité proposée pour l'API du planificateur. Elle permet de rendre le contrôle au thread principal plus facilement et mieux 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 rendu est un aspect inévitable de la logique de planification des tâches de JavaScript. Cela arrivera, la question est de savoir quand, et il vaut mieux que ce soit le plus tôt possible. Lorsque l'exécution des tâches prend trop de temps (plus de 50 millisecondes, pour être exact), elles sont considérées comme des tâches longues.

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 lance une tâche dans le navigateur ne signifie pas que vous devez attendre que cette tâche soit terminée avant de rendre le contrôle au thread principal. Vous pouvez améliorer la réactivité aux entrées utilisateur sur une page en renonçant explicitement à une tâche, ce qui la divise en plusieurs parties à terminer à la première occasion. Cela permet aux autres tâches d'utiliser le thread principal plus tôt 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. En résumé, le rendement est effectué de manière explicite, en divisant une tâche longue 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 l'arsenal 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 consiste à utiliser setTimeout avec une valeur de délai avant expiration de 0. Cela fonctionne, car le rappel transmis à setTimeout déplace 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 Glitch ou 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. Lorsque vous arrivez sur la page, procédez comme suit:

  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.

Vous remarquerez que le message suivant s'affiche dans le champ au bas de la démonstration:

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 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 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 que les développeurs hésitent à 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 important de se rappeler que l'abandon 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é ne sera pas relégué au second plan par rapport aux 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 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 ci-dessous 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.

La sortie dans le cadre en bas de la page ressemble à ceci:

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 souhaitez 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 souhaitez pouvoir définir des priorités de tâches et 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 vous préférez ne pas du tout céder, vous pouvez essayer une autre approche qui utilise scheduler.yield si elle est disponible, mais qui ne cède pas du tout si elle ne l'est pas:

// 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 à nos recherches pour nous aider à l'améliorer et nous faire part de vos commentaires sur la façon dont elle pourrait être améliorée.

Image principale issue de Unsplash, par Jonathan Allison.