Mesure de l'interaction avec la peinture suivante (INP)

1. Introduction

Cet atelier de programmation interactif vous permet d'apprendre à mesurer l'Interaction to Next Paint (INP) à l'aide de la bibliothèque web-vitals.

Prérequis

Objectifs de l'atelier

  • Ajouter la bibliothèque web-vitals à votre page et utiliser ses données d'attribution.
  • Utilisez les données d'attribution pour déterminer où et comment commencer à améliorer INP.

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/measuring-inp.
  3. Installez les dépendances: npm ci.
  4. Démarrez le serveur Web: npm run start.
  5. Vous pouvez saisir l'adresse http://localhost:8080/ dans votre navigateur.

Essayer la page

Cet atelier de programmation utilise Gastropodicon (un site de référence populaire sur l'anatomie des escargots) pour explorer les problèmes potentiels liés à l'INP.

Capture d'écran de la page de démonstration de Gastropodicon

Essayez d'interagir avec la page pour vous faire une idée des interactions lentes.

3. Positionnement dans 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.

Dans cet atelier de programmation, nous allons utiliser le panneau Performances et la console. Vous pouvez passer de l'un à l'autre à tout moment dans les onglets situés en haut des outils de développement.

  • Les problèmes d'INP se produisent le plus souvent sur les appareils mobiles. Par conséquent, passez à l'émulation d'affichage sur mobile.
  • Si vous effectuez le test sur un ordinateur de bureau ou portable, les performances seront probablement nettement meilleures que sur un véritable appareil mobile. Pour obtenir un aperçu des performances plus réaliste, cliquez sur l'icône en forme de roue dentée en haut à droite du panneau Performances, puis sélectionnez Ralentissement du processeur quatre fois plus.

Capture d'écran du panneau "Performances des outils de développement" à côté de l'application, avec l'option "Quatre ralentissement du processeur" sélectionnée

4. Installation Web Vitals

web-vitals est une bibliothèque JavaScript qui permet de mesurer les métriques Core Web Vitals auxquelles vos utilisateurs accèdent. Vous pouvez utiliser la bibliothèque pour capturer ces valeurs, puis les baliser sur un point de terminaison d'analyse pour une analyse ultérieure, afin que nous puissions déterminer où et quand les interactions lentes se produisent.

Il existe différentes manières d'ajouter la bibliothèque à une page. La manière dont vous allez installer la bibliothèque sur votre propre site dépendra de la manière dont vous gérez les dépendances, du processus de compilation et d'autres facteurs. N'oubliez pas de consulter la documentation de la bibliothèque pour connaître toutes les options qui s'offrent à vous.

Cet atelier de programmation s'installe à partir de npm et charge le script directement pour éviter de plonger dans un processus de compilation particulier.

Vous pouvez utiliser deux versions de web-vitals:

  • Le modèle "standard" doit être utilisé si vous souhaitez suivre les valeurs des métriques des métriques Core Web Vitals lors du chargement d'une page.
  • L'attribut "attribution" La compilation ajoute des informations de débogage à chaque métrique afin de déterminer pourquoi une métrique se termine avec la valeur qu'elle indique.

Pour mesurer l'INP dans cet atelier de programmation, nous voulons utiliser le build de l'attribution.

Ajoutez web-vitals au devDependencies du projet en exécutant npm install -D web-vitals

Ajoutez web-vitals à la page:

Ajoutez la version d'attribution du script en bas de index.html et consignez les résultats dans la console:

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log);
</script>

Essayer

Essayez à nouveau d'interagir avec la page en gardant la console ouverte. Lorsque vous cliquez sur la page, rien n'est consigné.

L'INP est mesuré tout au long du cycle de vie d'une page. Par défaut, web-vitals n'indique pas l'INP tant que l'utilisateur n'a pas quitté ou fermé la page. Il s'agit du comportement idéal pour le balisage pour une activité comme l'analyse, mais il l'est moins pour le débogage interactif.

web-vitals fournit une option reportAllChanges pour des rapports plus détaillés. Lorsque cette option est activée, toutes les interactions ne sont pas comptabilisées. En revanche, elles sont comptabilisées chaque fois qu'une interaction est plus lente que les précédentes.

Essayez d'ajouter l'option au script et d'interagir à nouveau avec la page:

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log, {reportAllChanges: true});
</script>

Actualisez la page. Les interactions devraient maintenant être signalées à la console, en se mettant à jour dès qu'une nouvelle plus lente est détectée. Par exemple, essayez de saisir du texte dans le champ de recherche, puis de supprimer l'entrée.

Capture d&#39;écran de la console DevTools avec les messages INP correctement affichés

5. Qu'est-ce qu'une attribution ?

Commençons par la toute première interaction que la plupart des utilisateurs auront avec la page : la boîte de dialogue de recueil du consentement aux cookies.

De nombreuses pages comportent des scripts qui nécessitent que les cookies soient déclenchés de manière synchrone lorsque les cookies sont acceptés par un utilisateur. Le clic devient alors une interaction lente. C'est ce qui se passe ici.

Cliquez sur Yes (Oui) pour accepter les cookies (démonstration) et examinez les données INP désormais consignées dans la console DevTools.

Objet de données INP enregistré dans la console DevTools

Ces informations générales sont disponibles dans les versions standards et pour l'attribution des statistiques Web Vitals:

{
  name: 'INP',
  value: 344,
  rating: 'needs-improvement',
  entries: [...],
  id: 'v4-1715732159298-8028729544485',
  navigationType: 'reload',
  attribution: {...},
}

Le temps écoulé à partir du moment où l'utilisateur a cliqué pour passer au pain suivant était de 344 millisecondes (une amélioration est nécessaire). INP Le tableau entries contient toutes les valeurs PerformanceEntry associées à cette interaction (dans le cas présent, un seul événement de clic).

Toutefois, pour savoir ce qui se passe au cours de cette période, nous nous intéressons surtout à la propriété attribution. Pour créer les données d'attribution, web-vitals détermine quelle image d'animation longue (LoAF) chevauche l'événement de clic. Le LoAF peut ensuite fournir des données détaillées sur le temps passé sur ce frame, depuis les scripts exécutés jusqu'au temps passé dans un rappel requestAnimationFrame, un style et une mise en page.

Développez la propriété attribution pour afficher plus d'informations. Les données sont bien plus riches.

attribution: {
  interactionTargetElement: Element,
  interactionTarget: '#confirm',
  interactionType: 'pointer',

  inputDelay: 27,
  processingDuration: 295.6,
  presentationDelay: 21.4,

  processedEventEntries: [...],
  longAnimationFrameEntries: [...],
}

Tout d'abord, il y a des informations sur les éléments avec lesquels les utilisateurs ont interagi:

  • interactionTargetElement: référence en direct à l'élément avec lequel l'utilisateur a interagi (si celui-ci n'a pas été supprimé du DOM).
  • interactionTarget: sélecteur permettant de trouver l'élément dans la page.

Ensuite, le calendrier est décomposé de manière générale:

  • inputDelay: temps écoulé entre le moment où l'utilisateur a commencé l'interaction (par exemple, un clic de souris) et le début de l'exécution de l'écouteur d'événements de cette interaction. Dans ce cas, le délai d'entrée n'était que d'environ 27 millisecondes, même avec la limitation du processeur activée.
  • processingDuration: temps nécessaire à l'exécution des écouteurs d'événements. Souvent, les pages comportent plusieurs écouteurs pour un même événement (par exemple, pointerdown, pointerup et click). Si elles s'exécutent toutes dans la même image d'animation, elles seront regroupées dans ce temps. Dans ce cas, la durée de traitement dure 295,6 millisecondes, la majeure partie du temps INP.
  • presentationDelay: temps écoulé entre le moment où les écouteurs d'événements sont terminés et le moment où le navigateur a fini de peindre le frame suivant. Dans ce cas, il s'agit de 21,4 millisecondes.

Ces phases INP peuvent être un signal vital pour diagnostiquer ce qui doit être optimisé. Vous trouverez plus d'informations à ce sujet dans le guide Optimize INP.

En explorant un peu plus le sujet, on constate que processedEventEntries contient cinq événements, par opposition à l'événement unique du tableau entries INP de premier niveau. Qu’est-ce qui les différencie ?

processedEventEntries: [
  {
    name: 'mouseover',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {
    name: 'mousedown',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {name: 'mousedown', ...},
  {name: 'mouseup', ...},
  {name: 'click', ...},
],

L'entrée de premier niveau est l'événement INP, dans ce cas un clic. Les processedEventEntries d'attribution correspondent à tous les événements qui ont été traités au cours du même frame. Notez qu'il inclut d'autres événements tels que mouseover et mousedown, en plus de l'événement de clic. Connaître ces autres événements peut être essentiel s'ils ont également été lents, car ils ont tous contribué à la lenteur de réactivité.

Enfin, il y a le tableau longAnimationFrameEntries. Il peut s'agir d'une seule entrée, mais dans certains cas, une interaction peut s'étaler sur plusieurs cadres. Voici le cas le plus simple, avec une seule longue image d'animation.

longAnimationFrameEntries

Développer l'entrée LoAF:

longAnimationFrameEntries: [{
  name: 'long-animation-frame',
  startTime: 1823,
  duration: 319,

  renderStart: 2139.5,
  styleAndLayoutStart: 2139.7,
  firstUIEventTimestamp: 1801.6,
  blockingDuration: 268,

  scripts: [{...}]
}],

Il y a un certain nombre de valeurs utiles ici, comme la répartition du temps passé à styliser. L'article sur l'API Long Animation Frames fournit des informations plus détaillées sur ces propriétés. Pour le moment, nous nous intéressons principalement à la propriété scripts, qui contient des entrées qui fournissent des détails sur les scripts responsables du frame de longue durée:

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 1828.6,
  executionStart: 1828.6,
  duration: 294,

  sourceURL: 'http://localhost:8080/third-party/cmp.js',
  sourceFunctionName: '',
  sourceCharPosition: 1144
}]

Dans ce cas, nous pouvons voir que le temps a été principalement passé dans un seul event-listener, appelé sur BUTTON#confirm.onclick. Nous pouvons même voir l'URL de la source du script et la position des caractères où la fonction a été définie.

Plats à emporter

Que pouvons-nous déterminer à propos de ce cas à partir de ces données d'attribution ?

  • L'interaction a été déclenchée par un clic sur l'élément button#confirm (à partir de attribution.interactionTarget et de la propriété invoker dans une entrée d'attribution de script).
  • Temps consacré principalement à l'exécution des écouteurs d'événements (entre attribution.processingDuration et value).
  • Le code de l'écouteur d'événements lents démarre à partir d'un écouteur de clics défini dans third-party/cmp.js (à partir de scripts.sourceURL).

Nous disposons de suffisamment de données pour identifier les points à optimiser.

6. Écouteurs d'événements multiples

Actualisez la page pour que la console DevTools soit claire et que l'interaction liée au consentement aux cookies ne soit plus la plus longue.

Commencez à saisir du texte dans le champ de recherche. Que montrent les données d'attribution ? Selon vous, que se passe-t-il ?

Données d'attribution

Commençons par une analyse générale d'un exemple de test de la démonstration:

{
  name: 'INP',
  value: 1072,
  rating: 'poor',
  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'keyboard',

    inputDelay: 3.3,
    processingDuration: 1060.6,
    presentationDelay: 8.1,

    processedEventEntries: [...],
    longAnimationFrameEntries: [...],
  }
}

Il s'agit d'une valeur INP faible (avec limitation du processeur activée) suite à une interaction du clavier avec l'élément input#search-terms. La majorité du temps (1 061 millisecondes sur un INP total de 1 072 millisecondes) a été consacrée à la durée de traitement.

Les entrées scripts sont cependant plus intéressantes.

Thrashing de mise en page

La première entrée du tableau scripts nous fournit un contexte précieux:

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 4875.6,
  executionStart: 4875.6,
  duration: 497,
  forcedStyleAndLayoutDuration: 388,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: 'handleSearch',
  sourceCharPosition: 940
},
...]

La majeure partie du traitement se produit lors de l'exécution de ce script, qui est un écouteur input (l'appelant est INPUT#search-terms.oninput). Le nom de la fonction est indiqué (handleSearch), tout comme la position du caractère dans le fichier source index.js.

Il existe cependant une nouvelle propriété: forcedStyleAndLayoutDuration. Il s'agit du temps passé sur l'appel de script pour lequel le navigateur a été contraint de remettre en page la page. En d'autres termes, 78% du temps (388 millisecondes sur 497) consacré à l'exécution de cet écouteur d'événements a été consacré au thrashing de mise en page.

Ce problème doit être résolu en priorité.

Écouteurs répétés

Individuellement, il n'y a rien de particulièrement remarquable dans les deux entrées de script suivantes:

scripts: [...,
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5375.3,
  executionStart: 5375.3,
  duration: 124,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526,
},
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5673.9,
  executionStart: 5673.9,
  duration: 95,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526
}]

Les deux entrées sont des écouteurs keyup, qui s'exécutent l'une après l'autre. Les écouteurs sont des fonctions anonymes (donc rien n'est signalé dans la propriété sourceFunctionName), mais nous disposons toujours d'un fichier source et d'une position de caractère pour pouvoir trouver l'emplacement du code.

Il est étrange que les deux proviennent du même fichier source et de la même position de caractère.

Le navigateur a fini par traiter plusieurs pressions de touche dans une seule image d'animation, ce qui a conduit cet écouteur d'événements à s'exécuter deux fois avant qu'aucun élément ne puisse être affiché.

Cet effet peut également s'aggraver : plus les écouteurs d'événements mettent du temps à s'exécuter, plus il y a d'événements d'entrée supplémentaires qui peuvent arriver, ce qui prolonge l'interaction lente beaucoup plus longtemps.

Puisqu'il s'agit d'une interaction de recherche/saisie semi-automatique, le rejet de la saisie serait une bonne stratégie, de sorte qu'une seule pression de touche soit traitée par frame au maximum.

7. Délai d'entrée

En général, les retards d'entrée sont dus au fait que le thread principal est occupé, c'est-à-dire le délai entre le moment où l'utilisateur interagit et le moment où un écouteur d'événements peut commencer à traiter l'interaction. Cela peut avoir plusieurs causes:

  • La page est en cours de chargement, et le thread principal est en train d'effectuer le travail initial de configuration du DOM, de la mise en page et du style de la page, ainsi que l'évaluation et l'exécution des scripts.
  • La page est généralement surchargée, par exemple pour l'exécution de calculs, d'animations basées sur des scripts ou d'annonces.
  • Le traitement des interactions précédentes est tellement long qu'ils retardent les interactions suivantes, comme dans l'exemple précédent.

La page de démonstration comporte une fonctionnalité secrète : lorsque vous cliquez sur le logo de l'escargot en haut de la page, l'animation démarre et génère un gros travail JavaScript dans le thread principal.

  • Cliquez sur le logo de l'escargot pour lancer l'animation.
  • Les tâches JavaScript sont déclenchées lorsque l'escargot se trouve dans la partie inférieure du rebond. Essayez d'interagir avec la page le plus près possible du bas du rebond et voyez jusqu'à quelle hauteur d'INP vous pouvez déclencher.

Par exemple, même si vous ne déclenchez aucun autre écouteur d'événements (comme si vous cliquez et sélectionnez le champ de recherche juste lorsque l'escargot rebondit), le thread principal empêchera la page de répondre pendant un certain temps.

Sur de nombreuses pages, le travail intensive du thread principal ne se comportera pas si bien, mais c'est une bonne démonstration pour voir comment il peut être identifiable dans les données d'attribution INP.

Voici un exemple d'attribution basée sur le ciblage du champ de recherche uniquement lors du rebond d'un escargot:

{
  name: 'INP',
  value: 728,
  rating: 'poor',

  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'pointer',

    inputDelay: 702.3,
    processingDuration: 4.9,
    presentationDelay: 20.8,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 2064.8,
      duration: 790,

      renderStart: 2065,
      styleAndLayoutStart: 2854.2,
      firstUIEventTimestamp: 0,
      blockingDuration: 740,

      scripts: [{...}]
    }]
  }
}

Comme prévu, les écouteurs d'événements s'exécutent rapidement : une durée de traitement de 4,9 millisecondes et la grande majorité de l'interaction médiocre a été consacrée au délai d'entrée, soit 702,3 millisecondes sur un total de 728.

Cette situation peut être difficile à déboguer. Même si nous savons avec quoi l'utilisateur a interagi et comment, nous savons également que cette partie de l'interaction s'est terminée rapidement et n'a pas posé de problème. C'est plutôt un autre élément de la page qui a retardé l'interaction du début du traitement, mais comment savoir par où commencer ?

Les entrées de script LoAF visent à sauver la mise:

scripts: [{
  name: 'script',
  invoker: 'SPAN.onanimationiteration',
  invokerType: 'event-listener',

  startTime: 2065,
  executionStart: 2065,
  duration: 788,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: 'cryptodaphneCoinHandler',
  sourceCharPosition: 1831
}]

Même si cette fonction n'avait aucun lien avec l'interaction, elle ralentit l'image de l'animation. Elle est donc incluse dans les données LoAF associées à l'événement d'interaction.

De là, nous pouvons voir comment la fonction qui a retardé le traitement de l'interaction a été déclenchée (par un écouteur animationiteration), exactement quelle fonction était responsable et où elle se trouvait dans nos fichiers sources.

8. Délai de présentation: lorsqu'une mise à jour ne fonctionne pas

Le délai de présentation mesure le temps écoulé entre le moment où les écouteurs d'événements ont fini de s'exécuter et le moment où le navigateur est en mesure d'afficher un nouveau frame à l'écran, montrant ainsi le retour visible de l'utilisateur.

Actualisez la page pour réinitialiser à nouveau la valeur INP, puis ouvrez le menu hamburger. Il y a un problème à l'ouverture.

Voici un exemple concret

{
  name: 'INP',
  value: 376,
  rating: 'needs-improvement',
  delta: 352,

  attribution: {
    interactionTarget: '#sidenav-button>svg',
    interactionType: 'pointer',

    inputDelay: 12.8,
    processingDuration: 14.7,
    presentationDelay: 348.5,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 651,
      duration: 365,

      renderStart: 673.2,
      styleAndLayoutStart: 1004.3,
      firstUIEventTimestamp: 138.6,
      blockingDuration: 315,

      scripts: [{...}]
    }]
  }
}

Cette fois, c'est le délai de présentation qui représente la majeure partie de l'interaction lente. Cela signifie que tout ce qui bloque le thread principal se produit une fois les écouteurs d'événements terminés.

scripts: [{
  entryType: 'script',
  invoker: 'FrameRequestCallback',
  invokerType: 'user-callback',

  startTime: 673.8,
  executionStart: 673.8,
  duration: 330,

  sourceURL: 'http://localhost:8080/js/side-nav.js',
  sourceFunctionName: '',
  sourceCharPosition: 1193,
}]

En examinant l'entrée unique du tableau scripts, nous constatons que le temps est passé dans un user-callback à partir d'un FrameRequestCallback. Cette fois, le délai de présentation est causé par un rappel requestAnimationFrame.

9. Conclusion

Agrégation des données de champ

Il convient de reconnaître que tout cela est plus facile lorsqu'on examine une seule entrée d'attribution INP à partir d'un seul chargement de page. Comment ces données peuvent-elles être agrégées pour déboguer INP en fonction des données de champ ? La quantité de détails utiles rend cela plus difficile.

Par exemple, il est extrêmement utile de savoir quel élément de la page est une source courante d'interactions lentes. Toutefois, si votre page contient des noms de classes CSS compilés qui changent d'une compilation à l'autre, les sélecteurs web-vitals du même élément peuvent être différents d'un build à l'autre.

Au lieu de cela, vous devez réfléchir à votre application spécifique pour déterminer ce qui est le plus utile et comment les données peuvent être agrégées. Par exemple, avant de baliser les données d'attribution, vous pouvez remplacer le sélecteur web-vitals par votre propre identifiant, en fonction du composant dans lequel se trouve la cible ou des rôles ARIA remplis par la cible.

De même, les chemins d'accès sourceURL des entrées scripts peuvent comporter des hachages basés sur des fichiers qui les rendent difficiles à combiner. Toutefois, vous pouvez supprimer les hachages en fonction de votre processus de compilation connu avant de baliser les données.

Malheureusement, il n'y a pas de chemin aisé avec des données aussi complexes, mais l'utilisation d'un sous-ensemble de ces données est plus utile que l'absence de données d'attribution pour le processus de débogage.

L'attribution partout !

L'attribution INP basée sur LoAF est une aide au débogage puissante. Il fournit des données précises sur ce qui s'est spécifiquement passé pendant un INP. Dans de nombreux cas, elle peut vous indiquer l'emplacement précis d'un script où vous devez commencer vos efforts d'optimisation.

Vous êtes désormais prêt à utiliser les données d'attribution INP sur n'importe quel site.

Même si vous n'êtes pas autorisé à modifier une page, vous pouvez recréer le processus à partir de cet atelier de programmation en exécutant l'extrait suivant dans la console DevTools pour voir ce que vous y trouverez:

const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
  webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);

En savoir plus