Comprendre l'INP (Interaction to Next Paint)

1. Introduction

Démonstration interactive et atelier de programmation pour en savoir plus sur l'Interaction to Next Paint (INP)

Schéma représentant une interaction sur le thread principal. L'utilisateur effectue une entrée tout en bloquant l'exécution des tâches. L'entrée est retardée jusqu'à ce que ces tâches soient terminées, après quoi les écouteurs d'événements de survol, de survol et de clic s'exécutent, puis le rendu et le dessin sont lancés jusqu'à ce que l'image suivante soit présentée.

Prérequis

  • Connaissance du développement HTML et JavaScript
  • Recommandation: lisez la documentation d'INP.

Objectifs

  • comment l'interaction des interactions des utilisateurs et votre gestion de ces interactions affectent la réactivité de la page ;
  • Comment réduire et éliminer les retards pour une expérience utilisateur fluide.

Ce dont vous avez besoin

  • Un ordinateur permettant de cloner du code à partir de GitHub et d'exécuter des commandes npm
  • Éditeur de texte.
  • Une version récente de Chrome pour que toutes les mesures des interactions 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. Parcourez le répertoire cloné: cd web-vitals-codelabs/understanding-inp
  3. Installer les dépendances: npm ci
  4. Démarrer le serveur Web: npm run start
  5. Accédez à l'adresse 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émonstration classique de la réactivité et de la réactivité !

Capture d'écran de l'application de démonstration utilisée dans cet atelier de programmation

Quatre mesures figurent sous le bouton:

  • INP: score INP actuel, qui est généralement la pire interaction.
  • Interaction: score de l'interaction la plus récente.
  • FPS: nombre d'images par seconde du thread principal de la page.
  • Minuteur: animation du minuteur en cours d'exécution pour aider à visualiser les à-coups.

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

Essayer

Essayez d'interagir avec le bouton Incrémenter et observez l'augmentation du score. Les valeurs INP et Interaction changent-elles à chaque incrément ?

L'INP mesure le temps qui s'écoule entre l'interaction de l'utilisateur et le moment où la page lui présente réellement la mise à jour affichée.

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

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

Accédez au panneau Performances, que vous utiliserez pour mesurer les interactions.

Capture d'écran du panneau "Performances" des outils de développement avec l'application

Enregistrez ensuite une interaction dans le panneau "Performances".

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

Dans la chronologie qui s'affiche, vous trouverez une piste Interactions. Pour la développer, cliquez sur le triangle situé à gauche.

Démonstration animée de l'enregistrement d'une interaction à l'aide du panneau des performances des outils de développement

Deux interactions apparaissent. Faites un zoom avant sur le deuxième élément en faisant défiler la page ou en maintenant la touche W enfoncée.

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

En passant la souris sur l'interaction, vous pouvez constater que celle-ci a été rapide et que la durée du traitement n'a pas été prise en compte, et que le délai d'entrée et le délai de présentation ont été minimes, 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 annulez la mise en commentaire de la fonction blockFor dans l'écouteur d'événements.

Afficher le code complet: click_block.html

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

Enregistrez le fichier. Le serveur va voir la modification et actualiser la page pour vous.

Réessayez d'interagir avec la page. Les interactions seront désormais nettement plus lentes.

Trace des performances

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

Une interaction d'une seconde dans le panneau "Performances"

Ce qui était auparavant une interaction courte prend désormais une seconde complète.

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 de l'écouteur 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 passe.

5. Test: durée de traitement

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

Commencez par mettre à jour l'UI

Que se passe-t-il si vous intervertissez l'ordre des appels JavaScript : mettez d'abord à jour l'interface utilisateur, puis bloquez-les ?

Afficher le code complet: ui_first.html

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

Avez-vous remarqué que l'interface utilisateur s'affiche plus tôt ? L'ordre affecte-t-il les scores INP ?

Essayez de prendre une trace et d'examiner l'interaction pour voir s'il y a des différences.

Écouteurs distincts

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

Consulter le code complet: two_click.html

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

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

À quoi cela ressemble maintenant dans le panneau des performances ?

Différents types d'événements

La plupart des interactions déclenchent de nombreux types d'événements, qu'il s'agisse de pointeurs ou d'événements clés, d'un survol, d'un focus ou d'un floutage, et d'événements synthétiques tels que beforechange et beforeinput.

De nombreuses pages réelles ont des écouteurs pour de nombreux événements différents.

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

Consulter le code complet: diff_handlers.html

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

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

Aucune mise à jour de l'interface utilisateur

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

Afficher le code complet: no_ui.html

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

6. Résultats du test sur la durée de traitement

Trace des performances: commencez par mettre à jour l'UI

Afficher le code complet: ui_first.html

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

Si vous cliquez sur le bouton dans le panneau "Performances", les résultats n'ont pas changé. Alors qu'une mise à jour de l'interface utilisateur était déclenchée avant le code de blocage, le navigateur n'a mis à jour ce qui était affiché à l'écran qu'une fois l'écouteur d'événements terminé, ce qui signifie que l'interaction prenait encore un peu plus d'une seconde.

Une interaction toujours d'une seconde dans le panneau "Performances"

Trace des performances: écouteurs distincts

Consulter le code complet: two_click.html

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

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

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

Si vous faites un zoom avant 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, qui consiste à mettre à jour l'interface utilisateur, s'exécute incroyablement vite, tandis que la seconde prend toute une seconde. Cependant, la somme de leurs effets produit la même interaction lente pour l'utilisateur final.

Un zoom avant sur l'interaction d'une seconde dans cet exemple, montrant que le premier appel de fonction prend moins d'une milliseconde

Trace 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 est toujours d'une seconde entière. La seule différence est que l'écouteur click plus court de mise à jour de l'interface utilisateur s'exécute désormais après l'écouteur pointerup bloquant.

Dans cet exemple, un zoom avant montre l'interaction d'une seconde, montrant que l'écouteur d'événements de clic prend moins d'une milliseconde pour s'exécuter, après l'écouteur de pointeurs.

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

Afficher le code complet: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • Le score ne se met pas à jour, mais la page le fait malgré tout.
  • 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 du texte continuent d'être mis à jour.

Dans ce cas, le bouton passe à un état actif et revient à l'état actif lorsque l'utilisateur clique dessus. Le navigateur doit alors repeindre le bouton, ce qui signifie qu'il reste un INP.

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

L'enregistrement d'un panneau "Performances" montre que l'interaction est pratiquement identique à celle qui s'est produite auparavant.

Une interaction toujours d'une seconde dans le panneau "Performances"

Plats à emporter

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

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

C'est un problème courant.

Enfin, le fait que votre code ne déclenche pas de peinture ne signifie pas qu'il n'attend pas que des écouteurs d'événements lents se terminent.

7. Test: délai d'entrée

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

  • Si un <script> de chargement tardif bloquait la page de manière aléatoire lors du chargement.
  • Un appel d'API, tel que setInterval, qui bloque régulièrement la page ?

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

Consulter 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 avec délai d'entrée

Consulter 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 bloquant n'est effectué dans l'interaction elle-même.

Ces longues périodes sont souvent appelées tâches longues.

En passant la souris sur l'interaction dans les outils de développement, vous pouvez constater que la durée d'interaction est désormais principalement attribuée au délai d'entrée, et non à la durée du traitement.

Panneau &quot;DevTools Performance&quot; affichant une tâche bloquante d&#39;une seconde, une interaction en cours d&#39;exécution et une interaction de 642 millisecondes, principalement attribuées au délai de saisie

Notez que cela n'affecte pas toujours les interactions. Si vous ne cliquez pas alors que la tâche est en cours d'exécution, vous pourriez avoir de la chance. Par exemple, elles sont "aléatoires" les éternuements peuvent être un cauchemar à déboguer lorsqu'ils ne causent que parfois des problèmes.

Pour les identifier, vous pouvez mesurer les longues tâches (ou longues images d'animation) et le temps total de blocage.

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 Next Paint ?

Eh bien, mettre à jour la page avec des effets coûteux !

Même si les pages sont mises à jour rapidement, le navigateur devra peut-être travailler dur pour les afficher !

Sur le thread principal:

  • Frameworks d'UI qui doivent effectuer les mises à jour après un changement d'état
  • Les modifications DOM ou l'activation/désactivation de nombreux sélecteurs de requêtes CSS coûteux peuvent déclencher de nombreuses modifications de style, de mise en page et de peinture.

En dehors du thread principal:

  • Utiliser du CSS pour optimiser les effets GPU
  • Ajouter de très grandes images haute résolution
  • Utiliser le format SVG/Canvas pour dessiner des scènes complexes

Une esquisse des différents éléments de rendu sur le Web

RenderingNG

Voici quelques exemples couramment utilisés sur le Web:

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

10. Test: retard 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 renvoi de l'écouteur d'événements:

Consulter le code complet: overview_delay.html

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

Que se passe-t-il ?

11. Résultats du test du délai de présentation

Consulter le code complet: overview_delay.html

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

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

requestAnimationFrame demande un rappel avant la peinture suivante. Étant donné que l'INP mesure le temps écoulé entre l'interaction et le rendu suivant, le blockFor(1000) de requestAnimationFrame continue de bloquer le pain suivant pendant une seconde entière.

Une interaction toujours d&#39;une seconde dans le panneau &quot;Performances&quot;

Notez toutefois deux points:

  • En pointant sur l'écran, vous verrez que la totalité du temps d'interaction est maintenant dépensée dans "Délai de présentation". puisque le blocage du thread principal se produit après le retour de l'écouteur d'événements.
  • La racine de l'activité du thread principal n'est plus l'événement de clic, mais "Frame d'animation déclenché".

12. Diagnostiquer les interactions

Sur cette page de test, la réactivité est super visuelle, avec les scores, les minuteurs et l'interface utilisateur du compteur. Mais lors du test de la page moyenne, elle est plus subtile.

Lorsque les interactions durent longtemps, il n'est pas toujours évident d'identifier le coupable. Est-ce:

  • Délai d'entrée ?
  • Durée du traitement des événements ?
  • Un retard dans la 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 ce processus:

  1. Naviguez sur le Web comme vous le feriez habituellement.
  2. Facultatif: Laissez la console des outils de développement ouverte pendant que l'extension Web Vitals consigne les interactions.
  3. Si vous constatez qu'une interaction n'est pas performante, essayez de la reproduire:
  • Si vous ne pouvez pas le répéter, utilisez les journaux de la console pour obtenir des insights.
  • Si vous pouvez le répéter, enregistrez-le dans le panneau des performances.

Tous les retards

Essayez d'ajouter à la page quelques-uns de ces problèmes:

Consulter 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 "Performances" pour diagnostiquer les problèmes.

13. Test: travail asynchrone

Étant donné que vous pouvez lancer 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 finissent par mettre à jour la page ?

Tant que l'élément next paint suivant 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 l'affichage, la mesure de l'interaction s'arrête.

Pour essayer, continuez à mettre à jour l'interface utilisateur à partir de l'écouteur de clics, mais exécutez la tâche bloquante à partir du délai avant expiration.

Consulter le code complet: expiration_100.html

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

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

Que va-t-il se passer ?

14. Résultats des tests de travail asynchrones

Consulter le code complet: expiration_100.html

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

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

Interaction de 27 millisecondes avec une tâche d&#39;une seconde qui se produit maintenant 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 longue tâche de blocage s'exécute toujours après l'application de la peinture. L'utilisateur reçoit donc immédiatement un retour d'information sur l'interface utilisateur.

Leçon: Si tu n'arrives pas à la supprimer, tu dois au moins la déplacer !

Méthodes

Peut-on faire mieux qu'un setTimeout fixe de 100 millisecondes ? Nous voulons probablement que le code s'exécute aussi rapidement que possible, sinon nous aurions dû le supprimer !

Objectif :

  • L'interaction sera exécutée incrementAndUpdateUI().
  • blockFor() s'exécutera dès que possible, mais ne bloquera pas la prochaine peinture.
  • Il en résulte un comportement prévisible sans "délais d'inactivité magiques".

Voici quelques façons d'y parvenir:

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

"requestPostAnimationFrame"

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

Afficher le code complet: raf+task.html

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

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

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

Pour des raisons d'ergonomie, vous pouvez même tenir toutes vos promesses:

Afficher 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 furtifs)

Il peut être utile de déplacer de longues solutions de blocage, mais elles bloquent tout de même la page, ce qui affecte les futures interactions, ainsi que de nombreuses autres animations et mises à jour de la page.

Essayez à nouveau d'utiliser la version de travail avec blocage asynchrone de la page (ou la vôtre si vous avez créé votre propre version sur le report des tâches à la dernière étape):

Consulter le code complet: expiration_100.html

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

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

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

Trace des 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 importante.

Plusieurs tâches d&#39;une seconde dans le thread principal, entraînant des interactions aussi lentes que 800 ms

Lorsque ces longues tâches se chevauchent avec l'arrivée de nouveaux clics, les interactions sont lentes, même si l'écouteur d'événements lui-même est renvoyé presque immédiatement. Nous avons créé la même situation que dans le test précédent concernant les délais de saisie. Cette fois, le délai d'entrée ne provient pas d'un élément setInterval, mais d'une tâche déclenchée par des écouteurs d'événements antérieurs.

Stratégies

Idéalement, nous voulons supprimer complètement les longues tâches !

  • Supprimez complètement le code inutile, en particulier les scripts.
  • Optimiser le code pour éviter d'exécuter de longues tâches
  • Annuler les tâches obsolètes lorsque de nouvelles interactions se produisent.

16. Stratégie 1: contre-rebond

Une stratégie classique. Chaque fois que des interactions se succèdent rapidement et que le traitement ou les effets de réseau sont coûteux, retardez volontairement le démarrage du travail afin que vous puissiez 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émarrage d'une tâche coûteuse, avec un minuteur, de 500 à 1 000 millisecondes par exemple.
  • Enregistrez l'identifiant du minuteur lors de cette opération.
  • Si une nouvelle interaction se produit, annulez le minuteur précédent avec clearTimeout.

Consulter le code complet: debounce.html

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

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

Trace des performances

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

Malgré plusieurs clics, une seule tâche blockFor finit par s'exécuter, en attendant qu'il n'y ait plus eu de clics pendant une seconde entière avant de s'exécuter. Pour les interactions intensives, comme la saisie de texte ou les éléments cibles susceptibles de générer plusieurs clics rapides, il s'agit de la stratégie idéale à utiliser par défaut.

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

Il est toutefois possible qu'un autre clic se produise juste après la fin du délai de réponse, qu'il se retrouve au milieu de cette longue tâche et qu'il devienne une interaction très lente en raison du délai d'entrée.

Idéalement, si une interaction intervient au milieu de notre tâche, nous souhaitons mettre en pause notre travail chargé afin que toute nouvelle interaction soit traitée immédiatement. Comment pouvons-nous y parvenir ?

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

Beaucoup de setTimeout

Première tentative: faites quelque chose de simple.

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

Cela permet au navigateur de planifier chaque tâche individuellement, et la saisie peut être plus prioritaire.

Plusieurs interactions, mais l&#39;ensemble du travail planifié a été décomposé en nombreuses tâches plus petites

Nous sommes revenus à un travail de cinq secondes complet 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 ne présente 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 responsives.

Cette stratégie fonctionne particulièrement bien pour planifier des points d'entrée distincts, par exemple si vous devez appeler de nombreuses fonctionnalités indépendantes au moment du chargement de l'application. Par défaut, il suffit de charger les scripts et de tout exécuter au moment de leur évaluation.

Cependant, cette stratégie ne fonctionne pas aussi bien pour décomposer du code étroitement couplé, comme une boucle for qui utilise un état partagé.

Maintenant avec yield()

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

Exemple :

Consulter le code complet: rendementy.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 précédemment, le thread principal est généré après un bloc de travail et le navigateur est en mesure de répondre à toutes les interactions entrantes. Toutefois, il suffit désormais d'un await schedulerDotYield() au lieu de setTimeout distinctes, 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 davantage de travail, même si de nouvelles interactions sont arrivées et ont pu changer le travail qui doit être fait.

Avec cette stratégie, nous annulons le délai avant expiration précédent à chaque nouvelle interaction. Pouvons-nous faire quelque chose de similaire ici ? Pour ce faire, vous pouvez utiliser un AbortController():

Afficher le code complet: abandonné.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 se produit, il lance la boucle for blockInPiecesYieldyAborty pour effectuer le travail nécessaire, tout en générant régulièrement le thread principal afin que le navigateur reste réactif aux nouvelles interactions.

Lorsqu'un second clic se produit, la première boucle est signalée comme annulée avec AbortController et une nouvelle boucle blockInPiecesYieldyAborty est lancée. La prochaine fois que la première boucle est planifiée pour s'exécuter à nouveau, elle remarque que signal.aborted est désormais true et est immédiatement renvoyé sans effectuer d'autres tâches.

Le travail du thread principal se compose maintenant de nombreux éléments minuscules, les interactions sont courtes et le travail ne dure que le temps nécessaire

18. Conclusion

La séparation de toutes les longues tâches permet au site d'être réactif aux nouvelles interactions. Cela vous permet de fournir rapidement des commentaires initiaux et de prendre des décisions, par exemple pour annuler un travail en cours. Cela implique parfois de planifier les points d'entrée en tant que tâches distinctes. Parfois, cela implique d'ajouter la colonne "rendement" lorsque cela est pratique.

À noter

  • INP mesure toutes les interactions.
  • Chaque interaction est mesurée entre l'entrée et la peinture suivante, c'est-à-dire la façon dont l'utilisateur voit la réactivité.
  • Le délai d'entrée, la durée de traitement des événements et le délai de présentation ont tous une incidence sur la réactivité de l'interaction.
  • Avec les outils de développement, vous pouvez facilement mesurer l'INP et la répartition des interactions.

Stratégies

  • Évitez les longues tâches de code (c'est-à-dire les longues tâches) sur vos pages.
  • Retirez le code inutile des écouteurs d'événements jusqu'à la fin du processus.
  • Assurez-vous que la mise à jour du rendu est efficace pour le navigateur.

En savoir plus