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 exploitent le code JavaScript pour afficher leur contenu doivent actuellement faire un compromis entre les performances de chargement et la réactivité des entrées: soit effectuer tout le travail nécessaire à l'affichage en une seule fois (meilleures performances de chargement, moins de réactivité des entrées), soit diviser le travail en tâches plus petites afin de rester réactif aux entrées et à la peinture (moins bonnes performances de chargement, meilleure réactivité des entrées).

Pour éviter d'avoir à faire ce compromis, Facebook a proposé et implémenté l'API isInputPending() dans Chromium afin d'améliorer la réactivité sans céder. Sur la base des commentaires reçus lors de la phase d'évaluation de l'API, nous avons apporté plusieurs modifications à l'API. Nous sommes heureux de vous 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 (la réactivité en particulier) peut être considérablement affectée 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 l'événement d'entrée de clic qu'une fois cette tâche 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 cède le contrôle au navigateur, il faut un certain temps pour que le navigateur vérifie sa file d'attente d'événements d'entrée, traite les événements et récupère le prochain bloc JavaScript. 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.

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, exclure les nombres premiers ou simplement dessiner un voyant de chargement sympa. Chacun d'eux est divisé en éléments de travail distincts. À 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 souhaitez également générer des revenus pour ces éléments, ce n'est pas un problème. En fournissant un objet à isInputPending() avec includeContinuous défini sur true, nous sommes prêts à partir:

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éécritures importantes.

La cession n'est pas toujours négative

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, le fait de définir des extraits et des masques complexes pour les iFrames inter-origines peut générer des faux négatifs (c'est-à-dire que isInputPending() peut renvoyer de manière inattendue la valeur "false" lors du ciblage de ces cadres). Assurez-vous de générer des résultats suffisamment souvent si votre site nécessite des interactions avec des sous-cadres stylisés.

Tenez également compte des 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 renverra jamais true si la saisie est distribuée à un frame inter-origines. Par conséquent, les pages en arrière-plan peuvent interférer avec la réactivité des pages au 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. Avec cette API, Facebook a créé une nouvelle API Web pour la faire passer de l'incubation d'idées à la proposition de normes, puis à la mise en service 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.