Amélioration de la planification JS avec isInputPending()

Une nouvelle API JavaScript qui peut vous aider à éviter le compromis entre les performances de chargement et la réactivité des entrées.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Il est difficile de charger rapidement. Les sites qui s'appuient actuellement sur JavaScript pour afficher leur contenu doivent actuellement faire un compromis entre les performances de chargement et la réactivité d'entrée: soit effectuer tout le travail nécessaire pour l'affichage en une seule fois (meilleures performances de chargement, moins bonne réactivité aux entrées), ou diviser le travail en tâches plus petites afin de rester réactif à l'entrée et à la peinture (moins de performances de chargement, meilleure réactivité aux entrées).

Pour éliminer ce compromis, Facebook a proposé et implémenté l'API isInputPending() dans Chromium afin d'améliorer la réactivité sans générer de rendement. Suite aux commentaires que nous avons reçus concernant la phase d'évaluation, nous avons apporté un certain nombre de mises à jour à l'API, et nous sommes heureux d'annoncer qu'elle est désormais disponible par défaut dans Chromium 87.

Compatibilité du navigateur

Navigateurs pris en charge

  • Chrome : 87
  • Edge: 87
  • Firefox: non compatible.
  • Safari : non compatible.

Source

isInputPending() est disponible dans les navigateurs Chromium à partir de la version 87. Aucun autre navigateur n'a signalé l'intention d'envoyer l'API.

Contexte

La plupart des tâches de l'écosystème JavaScript actuel sont effectuées sur un seul thread: le thread principal. Cela fournit aux développeurs un modèle d'exécution robuste, mais l'expérience utilisateur (en particulier la réactivité) peut en pâtir considérablement si le script s'exécute pendant une longue période. Par exemple, si la page effectue de nombreuses tâches lorsqu'un événement d'entrée est déclenché, elle ne gérera pas l'événement d'entrée de clic tant que cette tâche n'est pas terminée.

La bonne pratique actuelle consiste à résoudre ce problème en divisant le code JavaScript en blocs plus petits. Pendant le chargement de la page, celle-ci peut exécuter un peu de code JavaScript, puis céder le contrôle au navigateur. Le navigateur peut ensuite vérifier sa file d'attente d'événements d'entrée et voir s'il doit communiquer quelque chose à la page. Le navigateur peut ensuite reprendre l'exécution des blocs JavaScript à mesure qu'ils sont ajoutés. Cela peut aider, mais cela peut aussi entraîner d'autres problèmes.

Chaque fois que la page rend le contrôle au navigateur, il faut un certain temps pour que celui-ci vérifie sa file d'attente d'événements d'entrée, traite les événements et récupère le bloc JavaScript suivant. Bien que le navigateur réponde plus rapidement aux événements, le temps de chargement global de la page est ralenti. Et si nous cédons trop souvent, la page se charge trop lentement. Si nous cédons moins souvent, le navigateur met plus de temps à répondre aux événements utilisateur, ce qui peut frustrer les utilisateurs. Ce n'est pas amusant.

Schéma illustrant que lorsque vous exécutez des tâches JavaScript longues, le navigateur a moins de temps pour distribuer les événements.

Chez Facebook, nous voulions voir à quoi ressemblerait une nouvelle approche de chargement qui éliminerait ce compromis frustrant. Nous avons contacté nos amis de Chrome à ce sujet et avons proposé isInputPending(). L'API isInputPending() est la première à utiliser le concept d'interruptions pour les entrées utilisateur sur le Web et permet à JavaScript de vérifier les entrées sans céder au navigateur.

Un diagramme montrant que isInputPending() permet à votre code JavaScript de vérifier si une entrée utilisateur est en attente, sans renvoyer complètement l'exécution au navigateur.

L'API ayant suscité l'intérêt, nous avons collaboré avec nos collègues de Chrome pour implémenter et déployer la fonctionnalité dans Chromium. Avec l'aide des ingénieurs Chrome, nous avons déployé les correctifs dans le cadre d'un essai d'origine (qui permet à Chrome de tester les modifications et de recueillir les commentaires des développeurs avant de publier complètement une API).

Nous avons maintenant pris en compte les commentaires de l'évaluation de l'origine et des autres membres du groupe de travail sur les performances Web du W3C, et avons implémenté des modifications de l'API.

Exemple: un planificateur de yieldier

Supposons que vous deviez effectuer de nombreuses tâches bloquant l'affichage pour charger votre page, par exemple générer du balisage à partir de composants, factoriser des nombres premiers ou simplement dessiner un voyant de chargement sympa. Chacune d'entre elles est divisée en un élément de travail discret. À l'aide du modèle de planificateur, décrivons comment nous pourrions traiter notre travail dans une fonction processWorkQueue() hypothétique:

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
 
if (performance.now() >= DEADLINE) {
   
// Yield the event loop if we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

En appelant processWorkQueue() plus tard dans une nouvelle macrotâche via setTimeout(), nous permettons au navigateur de rester quelque peu réactif aux entrées (il peut exécuter des gestionnaires d'événements avant la reprise du travail) tout en parvenant à s'exécuter de manière relativement ininterrompue. Toutefois, nous pouvons être désorganisés pendant une longue période par d'autres tâches qui souhaitent contrôler la boucle d'événements ou atteindre une latence d'événement supplémentaire de QUANTUM millisecondes.

C'est bien, mais pouvons-nous faire mieux ? Tout à fait !

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
 
if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
   
// Yield if we have to handle an input event, or we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

En introduisant un appel à navigator.scheduling.isInputPending(), nous pouvons répondre plus rapidement aux entrées tout en nous assurant que notre travail de blocage de l'écran s'exécute sans interruption. Si nous ne souhaitons gérer que l'entrée (par exemple, la peinture) jusqu'à ce que le travail soit terminé, nous pouvons également augmenter facilement la longueur de QUANTUM.

Par défaut, les événements "continus" ne sont pas renvoyés à partir de isInputPending(). Cela inclut mousemove, pointermove et d'autres. Si vous voulez aussi en générer, pas de problème. En fournissant un objet à isInputPending() avec includeContinuous défini sur true, vous pouvez continuer:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
 
if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
   
// Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

Et voilà ! Des frameworks tels que React intègrent la prise en charge de isInputPending() dans leurs bibliothèques de planification principales à l'aide d'une logique similaire. Nous espérons que cela permettra aux développeurs qui utilisent ces frameworks de bénéficier de isInputPending() en coulisses sans réécriture importante.

La cession n'est pas toujours mauvaise

Il convient de noter que réduire la production n'est pas la solution idéale pour tous les cas d'utilisation. Il existe de nombreuses raisons de rendre le contrôle au navigateur en dehors du traitement des événements d'entrée, par exemple pour effectuer le rendu et exécuter d'autres scripts sur la page.

Il arrive que le navigateur ne parvienne pas à attribuer correctement les événements d'entrée en attente. En particulier, la définition de clips et de masques complexes pour des iFrames multi-origines peut signaler des faux négatifs (par exemple, isInputPending() peut renvoyer de manière inattendue la valeur "false" lors du ciblage de ces frames). Assurez-vous de générer des résultats suffisamment souvent si votre site nécessite des interactions avec des sous-cadres stylisés.

Faites également attention aux autres pages qui partagent une boucle d'événements. Sur des plates-formes telles que Chrome pour Android, il est assez courant que plusieurs origines partagent une boucle d'événements. isInputPending() ne renvoie jamais true si l'entrée est envoyée à un frame multi-origine. Par conséquent, les pages en arrière-plan peuvent interférer avec la réactivité des pages de premier plan. Vous pouvez réduire, reporter ou céder plus souvent lorsque vous effectuez des tâches en arrière-plan à l'aide de l'API Page Visibility.

Nous vous encourageons à utiliser isInputPending() avec discernement. Si aucune tâche bloquante pour l'utilisateur n'est à effectuer, soyez gentil avec les autres utilisateurs de la boucle d'événements en renonçant plus fréquemment. Les tâches longues peuvent être dangereuses.

Commentaires

  • Envoyez vos commentaires sur la spécification dans le dépôt is-input-pending.
  • Contactez @acomminos (l'un des auteurs de la spécification) sur Twitter.

Conclusion

Nous sommes ravis de lancer isInputPending() et de permettre aux développeurs de l'utiliser dès aujourd'hui. C'est la première fois que Facebook développe une nouvelle API Web, qui est passée de l'incubation d'idées à la proposition de normes et à la livraison dans un navigateur. Nous tenons à remercier tous ceux qui nous ont aidés à en arriver là, et à faire un clin d'œil spécial à tous les membres de l'équipe Chrome qui nous ont aidés à développer cette idée et à la mettre en œuvre.

Photo d'illustration par Will H McMahan sur Unsplash.