Amélioration de la planification JS avec isInputPending()

Nouvelle API JavaScript qui peut vous aider à éviter le compromis entre performances de chargement et réactivité aux entrées.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Le chargement rapide est difficile. Les sites qui utilisent JavaScript pour afficher leur contenu doivent actuellement faire un compromis entre performances de chargement et réactivité des entrées: soit effectuer tout le travail nécessaire pour l'affichage en une seule fois (meilleure performance de chargement, moins bonne réactivité aux entrées), ou diviser le travail en tâches plus petites afin de rester réactifs aux entrées et aux peintures (moins de 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 rendement. Suite aux commentaires que nous avons reçus concernant la phase d'évaluation, nous avons apporté plusieurs mises à jour à l'API et sommes heureux d'annoncer qu'elle est désormais disponible par défaut dans Chromium 87.

Compatibilité du navigateur

Navigateurs pris en charge

  • 87
  • 87
  • x
  • x

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

Contexte

Dans l'écosystème JavaScript actuel, la plupart des tâches sont effectuées sur un seul thread: le thread principal. Cela fournit un modèle d'exécution robuste aux développeurs, mais l'expérience utilisateur (en particulier la réactivité) peut en souffrir considérablement si le script s'exécute pendant une longue période. Par exemple, si la page effectue beaucoup de travail lors du déclenchement d'un événement d'entrée, elle ne gérera l'événement de saisie de clic qu'une fois l'opération terminée.

La bonne pratique actuelle consiste à résoudre ce problème en divisant le JavaScript en blocs plus petits. Pendant le chargement de la page, celle-ci peut exécuter un code JavaScript, puis renvoyer le contenu et transmettre le contrôle au navigateur. Le navigateur peut ensuite consulter sa file d'attente d'événements d'entrée et déterminer s'il doit fournir des informations à la page. Le navigateur peut ensuite recommencer à exécuter les blocs JavaScript au fur et à mesure qu'ils sont ajoutés. Cela peut être utile, mais cela peut causer d'autres problèmes.

Chaque fois que la page rend le contrôle au navigateur, celui-ci a besoin d'un certain temps pour vérifier sa file d'attente d'événements d'entrée, traiter les événements et récupérer le bloc JavaScript suivant. Alors que le navigateur répond plus rapidement aux événements, le temps de chargement global de la page est ralenti. Si le rendement est trop élevé, la page se charge trop lentement. Si nous abandonnons moins souvent, le navigateur met plus de temps à répondre aux événements utilisateur, et les utilisateurs sont frustrés. Ce n'est pas amusant.

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

Chez Facebook, nous voulions voir à quoi ressemblerait une nouvelle approche de chargement qui nous permettrait d'éliminer ce compromis frustrant. Nous en avons fait la demande auprès de nos amis de Chrome, qui nous ont proposé isInputPending(). L'API isInputPending() est la première à utiliser le concept d'interruption pour les entrées utilisateur sur le Web et permet à JavaScript de vérifier les entrées sans laisser le navigateur à disposition.

Schéma montrant qu'isInputPending() permet à votre JS de vérifier si une entrée utilisateur est en attente, sans renvoyer complètement l'exécution au navigateur.

Étant donné l'intérêt suscité par l'API, nous avons collaboré avec nos collègues de Chrome afin de l'implémenter et de la proposer dans Chromium. Avec l'aide des ingénieurs Chrome, nous avons obtenu les correctifs après une phase d'évaluation (qui permet à Chrome de tester les modifications et de recueillir les commentaires des développeurs avant de publier une API).

Nous avons maintenant pris en compte les commentaires de la phase d'évaluation et des autres membres du groupe de travail Web Performance W3C, et nous avons apporté des modifications à l'API.

Exemple: un planificateur plus productif

Supposons que vous ayez de nombreuses tâches à bloquer l'affichage pour charger votre page, par exemple en générant un balisage à partir de composants, en excluant les valeurs "premiers" ou en dessinant simplement une icône de chargement. Chacun d'entre eux est divisé en un élément de travail distinct. À l'aide du modèle de programmeur, esquissons 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() ultérieurement dans une nouvelle tâche macro via setTimeout(), nous permettons au navigateur de rester relativement réactif aux entrées (il peut exécuter des gestionnaires d'événements avant la reprise du travail) tout en gérant l'exécution sans interruption. Toutefois, nous pouvons être déprogrammés pendant longtemps par d'autres tâches qui souhaitent contrôler la boucle d'événements, ou obtenir jusqu'à QUANTUM millisecondes supplémentaires de latence des événements.

Ce n'est pas grave, mais pouvons-nous faire mieux ? Aucun problème.

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();
}

L'introduction d'un appel à navigator.scheduling.isInputPending() nous permet de répondre plus rapidement à la saisie, tout en nous assurant que notre tâche de blocage de l'affichage s'exécute sans interruption dans le cas contraire. Si nous ne souhaitons pas traiter autre chose 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 par isInputPending(). Il s'agit, entre autres, de mousemove et pointermove. Si vous souhaitez céder à celles-ci aussi, pas de problème. En fournissant un objet à isInputPending() avec includeContinuous défini sur true, nous pouvons commencer:

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à ! Les frameworks tels que React intègrent la prise en charge de isInputPending() dans leurs bibliothèques de planification principales en utilisant une logique similaire. Nous espérons que cela permettra aux développeurs qui utilisent ces frameworks de bénéficier de isInputPending() en arrière-plan sans réécritures importantes.

Le rendement n'est pas toujours mauvais

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

Il peut arriver que le navigateur ne soit pas en mesure d'attribuer correctement les événements d'entrée en attente. En particulier, la définition d'extraits et de masques complexes pour des iFrames multi-origines peut générer des faux négatifs (par exemple, isInputPending() peut renvoyer de manière inattendue la valeur "false" lors du ciblage de ces images). Veillez à ce que votre rendement soit suffisant 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 au premier plan. Vous pouvez souhaiter réduire, différer ou donner plus de rendement 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. S'il n'y a pas de tâche de blocage à effectuer, soyez gentil envers les autres dans la boucle des événements en renvoyant plus fréquemment. Les tâches longues peuvent être nuisibles.

Commentaires

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

Conclusion

Nous sommes ravis du lancement de isInputPending() et des développeurs qui peuvent commencer à l'utiliser dès aujourd'hui. C'est la première fois que Facebook crée une API Web et l'a fait passer de l'incubation d'idées à la proposition de normes, en passant par la livraison dans un navigateur. Nous tenons à remercier toutes les personnes qui nous ont aidés à en arriver là et à remercier toutes les personnes qui, chez Chrome, nous ont aidés à étoffer cette idée et à la livrer !

Photo principale de Will H McMahan sur Unsplash.