Comprendre l'INP (Interaction to Next Paint)

1. Introduction

Une démo interactive et un atelier de programmation pour en savoir plus sur l'Interaction to Next Paint (INP).

Diagramme représentant une interaction sur le thread principal. L'utilisateur saisit une entrée pendant l'exécution de tâches bloquantes. L'entrée est retardée jusqu'à ce que ces tâches soient terminées, après quoi les écouteurs d'événements pointerup, mouseup et click s'exécutent, puis le rendu et la peinture sont lancés jusqu'à ce que la prochaine frame soit présentée.

Prérequis

Objectifs

  • Comment l'interaction entre les interactions utilisateur et la façon dont vous les gérez affecte la réactivité de la page.
  • Comment réduire et éliminer les délais pour une expérience utilisateur fluide.

Ce dont vous avez besoin

  • Un ordinateur capable de cloner du code depuis GitHub et d'exécuter des commandes npm.
  • Un éditeur de texte.
  • disposer d'une version récente de Chrome pour que toutes les mesures d'interaction fonctionnent.

2. Configuration

Obtenir et exécuter le code

Le code se trouve dans le dépôt web-vitals-codelabs.

  1. Clonez le dépôt dans votre terminal : git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. Accédez au répertoire cloné : cd web-vitals-codelabs/understanding-inp
  3. Installez les dépendances : npm ci
  4. Démarrez le serveur Web : npm run start
  5. Accédez à http://localhost:5173/understanding-inp/ dans votre navigateur.

Présentation de l'application

En haut de la page se trouvent un compteur Score et un bouton Incrémenter. Une démo classique de réactivité et de réactivité !

Capture d'écran de l'application de démonstration pour cet atelier de programmation

Quatre mesures sont affichées sous le bouton :

  • INP : score INP actuel, qui correspond généralement à l'interaction la plus lente.
  • Interaction : score de l'interaction la plus récente.
  • FPS : nombre d'images par seconde du thread principal de la page.
  • Timer : animation de minuteur en cours pour visualiser le jank.

Les entrées "FPS" et "Timer" ne sont pas du tout nécessaires pour mesurer les interactions. Ils sont ajoutés uniquement pour faciliter la visualisation de la réactivité.

Essayer

Essayez d'interagir avec le bouton Increment et regardez le score augmenter. Les valeurs INP et Interaction changent-elles à chaque incrément ?

L'INP mesure le temps qui s'écoule entre le moment où l'utilisateur interagit avec la page et celui où la page affiche réellement la mise à jour rendue à l'utilisateur.

3. Mesurer les interactions avec les outils pour les développeurs Chrome

Ouvrez les outils de développement depuis le menu Plus d'outils > Outils de développement, en effectuant un clic droit sur la page et en sélectionnant Inspecter, ou en utilisant un raccourci clavier.

Passez au panneau Performances, que vous utiliserez pour mesurer les interactions.

Capture d'écran du panneau "Performances" des outils de développement à côté de l'application

Ensuite, capturez une interaction dans le panneau "Performances".

  1. Appuyez sur "Enregistrer".
  2. Interagissez avec la page (appuyez sur le bouton Increment).
  3. Arrêtez l'enregistrement.

La chronologie qui s'affiche contient une piste Interactions. Développez-le en cliquant sur le triangle à gauche.

Démonstration animée de l'enregistrement d'une interaction à l'aide du panneau "Performances" des outils pour les développeurs

Deux interactions s'affichent. Faites un zoom avant sur la deuxième en faisant défiler l'écran ou en maintenant la touche W enfoncée.

Capture d'écran du panneau "Performances" des outils de développement, avec le curseur pointant sur l'interaction dans le panneau et un info-bulle indiquant la durée de l'interaction

En pointant sur l'interaction, vous pouvez voir qu'elle a été rapide, sans temps passé dans la durée de traitement, et avec un temps minimal dans le délai d'entrée et le délai de présentation, dont la durée exacte dépend de la vitesse de votre machine.

4. Écouteurs d'événements de longue durée

Ouvrez le fichier index.js et supprimez la mise en commentaire de la fonction blockFor dans l'écouteur d'événements.

Voir le code complet : click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

Enregistrez le fichier. Le serveur verra la modification et actualisera la page pour vous.

Essayez d'interagir de nouveau avec la page. Les interactions seront désormais sensiblement plus lentes.

Trace de performances

Effectuez un autre enregistrement dans le panneau "Performances" pour voir à quoi cela ressemble.

Interaction d'une seconde dans le panneau "Performances"

Ce qui était auparavant une brève interaction prend désormais une seconde entière.

Lorsque vous pointez sur l'interaction, vous remarquerez que le temps est presque entièrement consacré à la "Durée de traitement", qui correspond au temps nécessaire pour exécuter les rappels du gestionnaire d'événements. Étant donné que l'appel blockFor bloquant se trouve entièrement dans l'écouteur d'événements, c'est là que le temps s'écoule.

5. Test : durée du traitement

Essayez de réorganiser le travail des écouteurs d'événements pour voir l'effet sur l'INP.

Mettre à jour l'UI en premier

Que se passe-t-il si vous inversez l'ordre des appels JS (mise à jour de l'UI, puis blocage) ?

Voir le code complet : ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

Avez-vous remarqué que l'UI s'est affichée plus tôt ? L'ordre a-t-il une incidence sur les scores INP ?

Essayez de faire un traçage et d'examiner l'interaction pour voir s'il y a des différences.

Séparer les écouteurs

Que se passe-t-il si vous déplacez le travail vers un écouteur d'événements distinct ? Mettez à jour l'UI dans un écouteur d'événements et bloquez la page à partir d'un autre écouteur.

Voir le code complet : two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

À quoi ressemble-t-il désormais dans le panneau "Performances" ?

Différents types d'événements

La plupart des interactions déclenchent de nombreux types d'événements, qu'il s'agisse d'événements de pointeur ou de clavier, ou d'événements de survol, de focus/blur et d'événements synthétiques tels que "beforechange" et "beforeinput".

De nombreuses pages réelles comportent des écouteurs pour différents événements.

Que se passe-t-il si vous modifiez les types d'événements pour les écouteurs d'événements ? Par exemple, remplacer l'un des écouteurs d'événements click par pointerup ou mouseup ?

Voir le code complet : diff_handlers.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

Aucune mise à jour de l'UI

Que se passe-t-il si vous supprimez l'appel à la mise à jour de l'UI de l'écouteur d'événements ?

Voir le code complet : no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});

6. Résultats des tests de durée de traitement

Trace de performances : mettre à jour l'UI en premier

Voir le code complet : ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

Si vous examinez l'enregistrement du panneau "Performances" lorsque vous cliquez sur le bouton, vous constaterez que les résultats n'ont pas changé. Bien qu'une mise à jour de l'UI ait été déclenchée avant le code bloquant, le navigateur n'a pas réellement mis à jour ce qui était affiché à l'écran avant la fin de l'écouteur d'événement. Cela signifie que l'interaction a quand même pris un peu plus d'une seconde.

Interaction d'une seconde dans le panneau "Performances"

Trace de performances : écouteurs séparés

Voir le code complet : two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

Une fois encore, il n'y a aucune différence fonctionnelle. L'interaction prend toujours une seconde entière.

Si vous zoomez sur l'interaction de clic, vous verrez que deux fonctions différentes sont appelées à la suite de l'événement click.

Comme prévu, la première (mise à jour de l'UI) s'exécute très rapidement, tandis que la seconde prend une seconde entière. Toutefois, la somme de leurs effets entraîne la même lenteur d'interaction pour l'utilisateur final.

Examen détaillé de l'interaction d'une seconde dans cet exemple, montrant que le premier appel de fonction prend moins d'une milliseconde

Tracé des performances : différents types d'événements

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

Ces résultats sont très similaires. L'interaction dure toujours une seconde entière. La seule différence est que l'écouteur click plus court, qui ne concerne que la mise à jour de l'UI, s'exécute désormais après l'écouteur pointerup bloquant.

Gros plan sur l'interaction d'une seconde dans cet exemple, montrant que l'écouteur d'événements de clic met moins d'une milliseconde à se terminer, après l'écouteur pointerup

Trace de performances : aucune mise à jour de l'UI

Voir le code complet : no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • Le score ne se met pas à jour, mais la page, si !
  • Les animations, les effets CSS, les actions par défaut des composants Web (saisie de formulaire), la saisie de texte et la mise en surbrillance de texte continuent de se mettre à jour.

Dans ce cas, le bouton passe à l'état actif, puis revient à l'état inactif lorsqu'on clique dessus. Le navigateur doit donc effectuer un rendu, ce qui signifie qu'il y a toujours un temps d'interaction avec la page.

Étant donné que le gestionnaire d'événements a bloqué le thread principal pendant une seconde, empêchant la page d'être peinte, l'interaction prend toujours une seconde entière.

L'enregistrement du panneau "Performances" montre une interaction pratiquement identique à celles qui l'ont précédée.

Interaction d'une seconde dans le panneau "Performances"

Plats à emporter

Tout code exécuté dans n'importe quel écouteur d'événements retardera l'interaction.

  • Cela inclut les écouteurs enregistrés à partir de différents scripts et du code de framework ou de bibliothèque qui s'exécute dans les écouteurs, comme une mise à jour d'état qui déclenche le rendu d'un composant.
  • Non seulement votre propre code, mais aussi tous les scripts tiers.

C'est un problème courant.

Enfin, ce n'est pas parce que votre code ne déclenche pas de peinture qu'une peinture n'attendra pas la fin des écouteurs d'événements lents.

7. Experiment: input delay

Qu'en est-il du code de longue durée en dehors des écouteurs d'événements ? Exemple :

  • Si vous aviez un <script> à chargement tardif qui bloquait la page de manière aléatoire pendant le chargement.
  • Un appel d'API, tel que setInterval, qui bloque périodiquement la page ?

Essayez de supprimer blockFor de l'écouteur d'événements et de l'ajouter à un setInterval() :

Voir le code complet : input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

Que se passe-t-il ?

8. Résultats du test de délai de réponse à l'entrée utilisateur

Voir le code complet : input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

L'enregistrement d'un clic sur un bouton qui se produit pendant l'exécution de la tâche de blocage setInterval entraîne une interaction de longue durée, même si aucun travail de blocage n'est effectué dans l'interaction elle-même.

Ces périodes de longue durée sont souvent appelées tâches longues.

Si vous pointez sur l'interaction dans les outils pour les développeurs, vous verrez que la durée de l'interaction est désormais principalement attribuée au délai d'entrée, et non à la durée de traitement.

Panneau &quot;Performances&quot; des outils de développement montrant une tâche de blocage d&#39;une seconde, une interaction se produisant à mi-parcours de cette tâche et une interaction de 642 millisecondes, principalement attribuée au délai d&#39;entrée

Notez que cela n'a pas toujours d'incidence sur les interactions. Si vous ne cliquez pas lorsque la tâche est en cours d'exécution, vous aurez peut-être de la chance. Ces éternuements "aléatoires" peuvent être un cauchemar à déboguer lorsqu'ils ne causent des problèmes que de temps en temps.

Pour les identifier, vous pouvez mesurer les tâches longues (ou Long Animation Frames) et le temps de blocage total.

9. Présentation lente

Jusqu'à présent, nous avons examiné les performances de JavaScript, via le délai d'entrée ou les écouteurs d'événements. Mais qu'est-ce qui affecte le rendu du prochain affichage ?

En mettant à jour la page avec des effets coûteux.

Même si la page est mise à jour rapidement, le navigateur peut avoir du mal à l'afficher.

Sur le thread principal :

  • Frameworks d'UI qui doivent afficher les mises à jour après les changements d'état
  • Les modifications du DOM ou l'activation/désactivation de nombreux sélecteurs de requête CSS coûteux peuvent déclencher de nombreux événements de style, de mise en page et de peinture.

Hors du thread principal :

  • Utiliser le CSS pour alimenter les effets GPU
  • Ajouter des images haute résolution très volumineuses
  • Utiliser SVG/Canvas pour dessiner des scènes complexes

Schéma des différents éléments de rendu sur le Web

RenderingNG

Voici quelques exemples courants sur le Web :

  • Un site SPA qui reconstruit l'intégralité du DOM après avoir cliqué sur un lien, sans s'arrêter pour fournir un retour visuel initial.
  • Une page de recherche qui propose des filtres de recherche complexes avec une interface utilisateur dynamique, mais qui exécute des écouteurs coûteux pour ce faire.
  • Un bouton d'activation du mode sombre qui déclenche le style/la mise en page pour l'ensemble de la page

10. Test : délai de présentation

Périphérique requestAnimationFrame lent

Simulons un long délai de présentation à l'aide de l'API requestAnimationFrame().

Déplacez l'appel blockFor dans un rappel requestAnimationFrame afin qu'il s'exécute après le retour de l'écouteur d'événements :

Voir le code complet : presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

Que se passe-t-il ?

11. Résultats des tests de délai de présentation

Voir le code complet : presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

L'interaction dure toujours une seconde. Que s'est-il passé ?

requestAnimationFrame demande un rappel avant le prochain affichage. Étant donné que l'INP mesure le temps écoulé entre l'interaction et le prochain affichage, le blockFor(1000) dans requestAnimationFrame continue de bloquer le prochain affichage pendant une seconde entière.

Interaction d&#39;une seconde dans le panneau &quot;Performances&quot;

Toutefois, notez deux choses :

  • Au survol, vous verrez que tout le temps d'interaction est désormais consacré au "délai de présentation", car le blocage du thread principal se produit après le retour du gestionnaire d'événements.
  • La racine de l'activité du thread principal n'est plus l'événement de clic, mais "Animation Frame Fired" (Frame d'animation déclenché).

12. Diagnostiquer les interactions

Sur cette page de test, la réactivité est très visuelle, avec les scores, les minuteurs et l'interface utilisateur du compteur. En revanche, elle est plus subtile sur la page de test de la moyenne.

Lorsque les interactions durent longtemps, il n'est pas toujours évident de savoir quelle en est la cause. Est-ce :

  • Délai de réponse à l'entrée utilisateur ?
  • Quelle est la durée de traitement des événements ?
  • Délai de présentation ?

Sur n'importe quelle page, vous pouvez utiliser les outils de développement pour mesurer la réactivité. Pour prendre cette habitude, essayez le flux suivant :

  1. Parcourez le Web comme d'habitude.
  2. Surveillez le journal des interactions dans la vue des métriques en direct du panneau "Performances" des outils de développement.
  3. Si vous constatez une interaction peu performante, essayez de la répéter :
  • Si vous ne parvenez pas à le reproduire, utilisez le journal des interactions pour obtenir des informations.
  • Si vous pouvez le reproduire, enregistrez une trace dans le panneau "Performances".

Tous les retards

Essayez d'ajouter un peu de chacun de ces problèmes à la page :

Voir le code complet : all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

Utilisez ensuite la console et le panneau des performances pour diagnostiquer les problèmes.

13. Test : travail asynchrone

Étant donné que vous pouvez démarrer des effets non visuels dans les interactions, comme effectuer des requêtes réseau, démarrer des minuteurs ou simplement mettre à jour l'état global, que se passe-t-il lorsque ces effets mettent finalement à jour la page ?

Tant que le next paint après une interaction est autorisé à s'afficher, même si le navigateur décide qu'il n'a pas réellement besoin d'une nouvelle mise à jour de rendu, la mesure de l'interaction s'arrête.

Pour essayer cela, continuez à mettre à jour l'UI à partir de l'écouteur de clics, mais exécutez le travail bloquant à partir du délai avant expiration.

Voir le code complet : timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

Que va-t-il se passer ?

14. Résultats du test de tâches asynchrones

Voir le code complet : timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

Une interaction de 27 millisecondes avec une tâche d&#39;une seconde qui se produit plus tard dans la trace

L'interaction est désormais courte, car le thread principal est disponible immédiatement après la mise à jour de l'UI. La tâche de blocage de longue durée s'exécute toujours, mais un peu après l'affichage. L'utilisateur reçoit donc un retour immédiat de l'UI.

Leçon : si vous ne pouvez pas le supprimer, au moins déplacez-le !

Méthodes

Peut-on faire mieux qu'un setTimeout fixe de 100 millisecondes ? Nous souhaitons probablement toujours que le code s'exécute le plus rapidement possible, sinon nous l'aurions simplement supprimé.

Objectif :

  • L'interaction s'exécutera incrementAndUpdateUI().
  • blockFor() s'exécutera dès que possible, mais ne bloquera pas la prochaine mise à jour de l'affichage.
  • Cela permet d'obtenir un comportement prévisible sans "délai magique".

Voici quelques exemples :

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

"requestPostAnimationFrame"

Contrairement à requestAnimationFrame seul (qui tentera de s'exécuter avant le prochain affichage et qui entraînera généralement une interaction lente), requestAnimationFrame + setTimeout constitue un simple polyfill pour requestPostAnimationFrame, exécutant le rappel après le prochain affichage.

Voir le code complet : raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  afterNextPaint(() => {
    blockFor(1000);
  });
});

Pour plus d'ergonomie, vous pouvez même l'encapsuler dans une promesse :

Voir le code complet : raf+task2.html

async function nextPaint() {
  return new Promise(resolve => afterNextPaint(resolve));
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  await nextPaint();
  blockFor(1000);
});

15. Interactions multiples (et clics de rage)

Déplacer les tâches de blocage longues peut aider, mais ces tâches longues bloquent toujours la page, ce qui affecte les interactions futures ainsi que de nombreuses autres animations et mises à jour de la page.

Essayez à nouveau la version de la page avec blocage asynchrone (ou la vôtre si vous avez trouvé votre propre variante pour différer le travail à la dernière étape) :

Voir le code complet : timeout_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

Que se passe-t-il si vous cliquez plusieurs fois rapidement ?

Trace de performances

Pour chaque clic, une tâche d'une seconde est mise en file d'attente, ce qui garantit que le thread principal est bloqué pendant une durée considérable.

Plusieurs tâches de plusieurs secondes dans le thread principal, entraînant des interactions aussi lentes que 800 ms

Lorsque ces tâches longues se chevauchent avec de nouveaux clics, les interactions sont lentes, même si l'écouteur d'événements lui-même renvoie une réponse presque immédiatement. Nous avons créé la même situation que dans l'expérience précédente avec les délais d'entrée. Cette fois-ci, le délai d'entrée ne provient pas d'un setInterval, mais d'un travail déclenché par des écouteurs d'événements précédents.

Stratégies

Dans l'idéal, nous souhaitons supprimer complètement les tâches longues.

  • Supprimez complètement le code inutile, en particulier les scripts.
  • Optimisez le code pour éviter d'exécuter des tâches longues.
  • Abandonnez les tâches obsolètes lorsque de nouvelles interactions arrivent.

16. Stratégie 1 : Débounce

Une stratégie classique. Chaque fois que des interactions arrivent rapidement les unes après les autres et que les effets de traitement ou de réseau sont coûteux, retardez volontairement le démarrage du travail afin de pouvoir l'annuler et le redémarrer. Ce modèle est utile pour les interfaces utilisateur telles que les champs de saisie semi-automatique.

  • Utilisez setTimeout pour retarder le début des tâches coûteuses, avec un minuteur, peut-être de 500 à 1 000 millisecondes.
  • Enregistrez l'ID du minuteur lorsque vous le faites.
  • Si une nouvelle interaction arrive, annulez le minuteur précédent à l'aide de clearTimeout.

Voir le code complet : debounce.html

let timer;
button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    blockFor(1000);
  }, 1000);
});

Trace de performances

Plusieurs interactions, mais une seule tâche longue de travail résultant de toutes ces interactions

Malgré plusieurs clics, une seule tâche blockFor finit par s'exécuter, en attendant qu'il n'y ait plus de clics pendant une seconde entière avant de s'exécuter. Pour les interactions qui se produisent par rafales (comme la saisie dans un champ de texte ou les cibles d'éléments qui sont censées recevoir plusieurs clics rapides), il s'agit d'une stratégie idéale à utiliser par défaut.

17. Stratégie 2 : interrompre les tâches de longue durée

Il existe toujours un risque que l'utilisateur clique à nouveau juste après la période d'antirebond, au milieu de la longue tâche, ce qui rend l'interaction très lente en raison du délai d'entrée.

Idéalement, si une interaction se produit au milieu de notre tâche, nous voulons mettre en pause notre travail afin que toute nouvelle interaction soit traitée immédiatement. Comment faire ?

Il existe des API comme isInputPending, mais il est généralement préférable de diviser les longues tâches en plusieurs parties.

Beaucoup de setTimeout

Première tentative : faites quelque chose de simple.

Voir le code complet : small_tasks.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
  });
});

Pour ce faire, le navigateur peut planifier chaque tâche individuellement, et les entrées peuvent avoir une priorité plus élevée.

Plusieurs interactions, mais tout le travail planifié a été divisé en de nombreuses tâches plus petites

Nous revenons à cinq secondes de travail pour cinq clics, mais chaque tâche d'une seconde par clic a été divisée en dix tâches de 100 millisecondes. Par conséquent, même si plusieurs interactions se chevauchent avec ces tâches, aucune interaction n'a de délai d'entrée supérieur à 100 millisecondes. Le navigateur donne la priorité aux écouteurs d'événements entrants par rapport au travail setTimeout, et les interactions restent réactives.

Cette stratégie fonctionne particulièrement bien lorsque vous planifiez des points d'entrée distincts, par exemple si vous avez un ensemble de fonctionnalités indépendantes que vous devez appeler au moment du chargement de l'application. Le simple chargement de scripts et l'exécution de tout au moment de l'évaluation des scripts peuvent exécuter tout dans une longue tâche géante par défaut.

Toutefois, cette stratégie ne fonctionne pas aussi bien pour séparer le code étroitement couplé, comme une boucle for qui utilise un état partagé.

Maintenant avec yield()

Toutefois, nous pouvons tirer parti des async et await modernes pour ajouter facilement des "points de rendement" à n'importe quelle fonction JavaScript.

Exemple :

Voir le code complet : yieldy.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

Comme auparavant, le thread principal est cédé après un bloc de travail et le navigateur peut répondre à toute interaction entrante. Toutefois, il suffit désormais d'un await schedulerDotYield() au lieu de setTimeout distincts, ce qui le rend suffisamment ergonomique pour être utilisé même au milieu d'une boucle for.

Maintenant avec AbortContoller()

Cela a fonctionné, mais chaque interaction planifie plus de travail, même si de nouvelles interactions sont arrivées et ont pu modifier le travail à effectuer.

Avec la stratégie de suppression des rebonds, nous avons annulé le délai d'attente précédent à chaque nouvelle interaction. Peut-on faire quelque chose de similaire ici ? Pour ce faire, vous pouvez utiliser un AbortController() :

Voir le code complet : aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

Lorsqu'un clic est effectué, il lance la boucle blockInPiecesYieldyAborty for, qui effectue le travail nécessaire tout en cédant périodiquement le thread principal afin que le navigateur reste réactif aux nouvelles interactions.

Lorsqu'un deuxième clic est effectué, la première boucle est marquée comme annulée avec AbortController et une nouvelle boucle blockInPiecesYieldyAborty est démarrée. La prochaine fois que la première boucle est programmée pour s'exécuter à nouveau, elle remarque que signal.aborted est maintenant true et renvoie immédiatement sans effectuer d'autres tâches.

Le travail du thread principal est désormais divisé en de nombreux petits morceaux, les interactions sont courtes et le travail ne dure que le temps nécessaire.

18. Conclusion

La découpe de toutes les tâches longues permet à un site de répondre aux nouvelles interactions. Cela vous permet de fournir rapidement des commentaires initiaux et de prendre des décisions, comme interrompre un travail en cours. Cela signifie parfois planifier les points d'entrée comme des tâches distinctes. Parfois, cela signifie ajouter des points de rendement là où c'est pratique.

À noter

  • L'INP mesure toutes les interactions.
  • Chaque interaction est mesurée de l'entrée à la prochaine peinture, c'est-à-dire la façon dont l'utilisateur perçoit la réactivité.
  • Le délai de réponse à l'entrée utilisateur, la durée de traitement des événements et le délai de présentation ont tous un impact sur la réactivité des interactions.
  • Vous pouvez facilement mesurer l'INP et les détails des interactions avec les outils de développement.

Stratégies

  • N'incluez pas de code de longue durée (tâches longues) sur vos pages.
  • Déplacez le code inutile hors des écouteurs d'événements jusqu'à la prochaine peinture.
  • Assurez-vous que la mise à jour du rendu elle-même est efficace pour le navigateur.

En savoir plus