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

1. Introduction

Cet atelier de programmation interactif vous explique comment mesurer l'interaction jusqu'à la prochaine peinture (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 identifier où et comment commencer à améliorer l'INP.

Ce dont vous avez besoin

  • Un ordinateur capable de cloner du code à partir de GitHub et d'exécuter des commandes npm.
  • Un éditeur de texte
  • 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/measuring-inp.
  3. Installez les dépendances: npm ci.
  4. Démarrez le serveur Web: npm run start.
  5. Accédez à 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 identifier les interactions lentes.

3. S'orienter dans les outils pour les développeurs Chrome

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

Dans cet atelier de programmation, nous utiliserons à la fois le panneau Performances et la console. Vous pouvez passer de l'un à l'autre à tout moment dans les onglets situés en haut de DevTools.

  • Les problèmes d'INP surviennent le plus souvent sur les appareils mobiles. Nous vous recommandons donc de passer à l'émulation d'écran mobile.
  • Si vous effectuez des tests sur un ordinateur de bureau ou portable, les performances seront probablement bien meilleures que sur un appareil mobile réel. Pour obtenir un aperçu plus réaliste des performances, cliquez sur la roue dentée en haut à droite du panneau Performances, puis sélectionnez Ralentir le processeur de 4 fois.

Capture d'écran du panneau "Performances" de DevTools à côté de l'application, avec un ralentissement du processeur de 4 fois sélectionné

4. Installer Web Vitals

web-vitals est une bibliothèque JavaScript qui permet de mesurer les métriques Web Vitals que vos utilisateurs rencontrent. Vous pouvez utiliser la bibliothèque pour capturer ces valeurs, puis les transmettre à un point de terminaison d'analyse pour une analyse ultérieure. Dans notre cas, cela permet de déterminer quand et où les interactions lentes se produisent.

Il existe plusieurs façons d'ajouter la bibliothèque à une page. La manière dont vous allez installer la bibliothèque sur votre propre site dépend de la façon dont vous gérez les dépendances, le processus de compilation et d'autres facteurs. Consultez la documentation de la bibliothèque pour connaître toutes les options disponibles.

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

Vous pouvez utiliser deux versions de web-vitals:

  • La version "standard" doit être utilisée si vous souhaitez suivre les valeurs des métriques Core Web Vitals lors du chargement d'une page.
  • La version "attribution" ajoute des informations de débogage supplémentaires à chaque métrique pour diagnostiquer pourquoi une métrique a la valeur qu'elle a.

Pour mesurer l'INP dans cet atelier de programmation, nous souhaitons utiliser la version de compilation d'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 au 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 d'interagir à nouveau avec la page lorsque la console est ouverte. Lorsque vous cliquez sur la page, rien n'est enregistré.

L'INP est mesuré tout au long du cycle de vie d'une page. Par défaut, web-vitals ne le signale donc pas tant que l'utilisateur ne quitte pas ou ne ferme pas la page. Il s'agit du comportement idéal pour la balise pour des éléments tels que l'analyse, mais il est moins adapté au débogage interactif.

web-vitals fournit l'option reportAllChanges pour générer des rapports plus détaillés. Lorsque cette option est activée, toutes les interactions ne sont pas enregistrées, mais chaque fois qu'une interaction est plus lente que la précédente, elle est enregistrée.

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 enregistrées dans la console et mises à jour chaque fois qu'une nouvelle interaction est la plus lente. Par exemple, essayez de saisir du texte dans le champ de recherche, puis de le supprimer.

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

5. Que contient 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 consentement aux cookies.

De nombreux scripts nécessitent que les cookies soient déclenchés de manière synchrone lorsque l'utilisateur les accepte, ce qui ralentit l'interaction. C'est ce qui se passe ici.

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

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

Ces informations de niveau supérieur sont disponibles dans les builds Web Vitals standards et d'attribution:

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

Le délai entre le clic de l'utilisateur et la peinture suivante était de 344 millisecondes, ce qui correspond à une INP "à améliorer". Le tableau entries contient toutes les valeurs PerformanceEntry associées à cette interaction (dans ce cas, un seul événement de clic).

Pour savoir ce qui se passe pendant cette période, nous nous intéressons principalement à la propriété attribution. Pour créer les données d'attribution, web-vitals identifie le cadre d'animation long (LoAF) qui chevauche l'événement de clic. Le LoAF peut ensuite fournir des données détaillées sur le temps passé pendant 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 beaucoup plus riches.

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

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

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

Tout d'abord, vous trouverez des informations sur l'élément avec lequel l'utilisateur a interagi:

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

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

  • inputDelay: temps écoulé entre le début de l'interaction par l'utilisateur (par exemple, un clic de souris) et le début de l'exécution de l'écouteur d'événement pour cette interaction. Dans ce cas, le délai d'entrée n'était que d'environ 27 millisecondes, même avec le débit du processeur limité.
  • processingDuration: temps nécessaire à l'exécution des écouteurs d'événements. Les pages comportent souvent plusieurs écouteurs pour un même événement (par exemple, pointerdown, pointerup et click). S'ils s'exécutent tous dans le même frame d'animation, ils seront fusionnés à ce moment-là. Dans ce cas, la durée de traitement est de 295,6 millisecondes, soit la majeure partie du temps d'INP.
  • presentationDelay: temps écoulé entre la fin des écouteurs d'événements et la fin de la peinture du frame suivant par le navigateur. Dans ce cas, 21,4 millisecondes.

Ces phases d'INP peuvent être un signal essentiel pour diagnostiquer ce qui doit être optimisé. Pour en savoir plus, consultez le guide sur l'optimisation de l'INP.

En creusant un peu plus, on constate que processedEventEntries contient cinq événements, contrairement à l'événement unique du tableau entries de l'INP de niveau supérieur. 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. L'attribution processedEventEntries correspond à l'ensemble des événements traités au cours du même frame. Notez qu'il inclut d'autres événements tels que mouseover et mousedown, et pas seulement l'événement de clic. La connaissance de ces autres événements peut être essentielle s'ils étaient également lents, car ils ont tous contribué à ralentir la réactivité.

Enfin, il y a le tableau longAnimationFrameEntries. Il peut s'agir d'une seule entrée, mais il arrive qu'une interaction s'étende sur plusieurs cadres. Voici le cas le plus simple, avec un seul frame d'animation long.

longAnimationFrameEntries

Développez l'entrée de la LoAF:

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

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

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

Vous trouverez ici un certain nombre de valeurs utiles, comme le temps passé à styliser. L'article sur l'API Long Animation Frames fournit plus d'informations 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 informations 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 pouvez-vous 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 sur une entrée d'attribution de script).
  • Le temps a été principalement consacré à l'exécution des écouteurs d'événements (attribution.processingDuration par rapport à la métrique totale value).
  • Le code de l'écouteur d'événement lent commence à partir d'un écouteur de clic défini dans third-party/cmp.js (à partir de scripts.sourceURL).

Nous avons suffisamment de données pour savoir où nous devons optimiser.

6. Écouteurs d'événements multiples

Actualisez la page pour que la console DevTools soit claire et que l'interaction de 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 ? Que se passe-t-il ?

Données d'attribution

Tout d'abord, un aperçu général 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 mauvaise valeur INP (avec le débit du processeur activé) issue d'une interaction avec le clavier avec l'élément input#search-terms. La majeure partie du temps (1 061 millisecondes sur 1 072 millisecondes au total) a été consacrée à la durée de traitement.

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

Déséquilibre de la mise en page

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

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 de la durée de 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), ainsi que la position des caractères dans le fichier source index.js.

Toutefois, il existe une nouvelle propriété: forcedStyleAndLayoutDuration. Il s'agit du temps passé lors de cette invocation de script, où le navigateur a été contraint de recomposer la page. En d'autres termes, 78% du temps (388 millisecondes sur 497) passé à exécuter cet écouteur d'événements a été consacré à la réorganisation de la mise en page.

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

Écouteurs répétés

Prises individuellement, les deux entrées de script suivantes n'ont rien de particulièrement remarquable:

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 (par conséquent, rien n'est signalé dans la propriété sourceFunctionName), mais nous disposons toujours d'un fichier source et d'une position de caractère, ce qui nous permet de trouver l'emplacement du code.

Ce qui est étrange, c'est 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 sur les touches dans un seul frame d'animation, ce qui a entraîné l'exécution de cet écouteur d'événement deux fois avant que quoi que ce soit ne puisse être peint.

Cet effet peut également s'accumuler. Plus la durée d'exécution des écouteurs d'événements est longue, plus les événements d'entrée supplémentaires peuvent arriver, ce qui prolonge l'interaction lente.

Étant donné qu'il s'agit d'une interaction de recherche/saisie semi-automatique, il est recommandé de déboguer la saisie afin qu'une seule touche soit traitée par frame.

7. Délai de réponse à l'entrée utilisateur

La raison habituelle des retards d'entrée (temps écoulé entre l'interaction de l'utilisateur et le moment où un écouteur d'événement peut commencer à traiter l'interaction) est que le thread principal est occupé. Plusieurs raisons peuvent expliquer ce problème:

  • La page est en cours de chargement et le thread principal effectue la tâche initiale de configuration du DOM, de mise en page et de stylisation de la page, ainsi que d'évaluation et d'exécution des scripts.
  • La page est généralement occupée (par exemple, elle exécute des calculs, des animations basées sur des scripts ou des annonces).
  • Le traitement des interactions précédentes prend tellement de temps qu'il retarde les interactions futures, comme illustré dans le dernier exemple.

La page de démonstration comporte une fonctionnalité secrète : si vous cliquez sur le logo de l'escargot en haut de la page, elle commence à s'animer et à effectuer des tâches JavaScript lourdes 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 est en bas du rebond. Essayez d'interagir avec la page le plus près possible du point de rebond et de voir quel niveau d'INP vous pouvez déclencher.

Par exemple, même si vous ne déclenchez aucun autre écouteur d'événement (par exemple, en cliquant sur le champ de recherche et en le mettant en surbrillance au moment où la limace rebondit), le travail du thread principal entraînera une indisponibilité de la page pendant une durée notable.

Sur de nombreuses pages, le travail lourd du thread principal ne sera pas aussi bien géré, mais cette démonstration montre comment il peut être identifiable dans les données d'attribution INP.

Voici un exemple d'attribution lorsque le focus est uniquement placé sur le champ de recherche lors d'un rebond lent:

{
  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 ont été exécutés rapidement, avec une durée de traitement de 4,9 millisecondes. 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 qu'elle n'a pas posé de problème. En réalité, c'est un autre élément de la page qui a retardé le début du traitement de l'interaction. Mais comment savoir par où commencer ?

Les entrées de script LoAF sont là pour vous aider:

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 rien à voir avec l'interaction, elle a ralenti le frame d'animation et est donc incluse dans les données LoAF associées à l'événement d'interaction.

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

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

Le délai de présentation mesure le temps écoulé entre la fin de l'exécution des écouteurs d'événements et le moment où le navigateur peut peindre un nouveau frame à l'écran, ce qui permet à l'utilisateur de voir le retour.

Actualisez la page pour réinitialiser à nouveau la valeur de l'INP, puis ouvrez le menu hamburger. Il y a un problème lors de 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 ce qui bloque le thread principal se produit après la fin des écouteurs d'événements.

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 la seule entrée du tableau scripts, nous voyons que le temps est passé dans un user-callback à partir d'un FrameRequestCallback. Cette fois, le délai de présentation est dû à un rappel requestAnimationFrame.

9. Conclusion

Agrégation des données de terrain

Il est important de noter que tout cela est plus facile à comprendre lorsque vous examinez 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 l'INP en fonction des données sur le terrain ? La quantité de détails utiles rend cette tâche encore 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 les noms de classe CSS compilés de votre page changent d'une version à l'autre, les sélecteurs web-vitals du même élément peuvent être différents d'une version à l'autre.

Vous devez plutôt 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 renvoyer les données d'attribution des balises, vous pouvez remplacer le sélecteur web-vitals par un identifiant de votre choix, en fonction du composant dans lequel se trouve la cible ou des rôles ARIA qu'elle remplit.

De même, les entrées scripts peuvent contenir des hachages basés sur des fichiers dans leurs chemins sourceURL, ce qui les rend difficiles à combiner. Toutefois, vous pouvez supprimer les hachages en fonction de votre processus de compilation connu avant de renvoyer les données.

Malheureusement, il n'existe pas de solution simple pour des données aussi complexes. Toutefois, même l'utilisation d'un sous-ensemble de ces données est plus utile que l'absence totale de données d'attribution pour le processus de débogage.

Attribution partout !

L'attribution INP basée sur LoAF est un outil de débogage puissant. Il fournit des données précises sur ce qui s'est passé précisément lors d'un INP. Dans de nombreux cas, il peut vous indiquer l'emplacement précis dans un script où vous devez commencer vos efforts d'optimisation.

Vous pouvez à présent utiliser les données d'attribution INP sur n'importe quel site.

Même si vous n'avez pas accès à la modification d'une page, vous pouvez recréer le processus de cet atelier de programmation en exécutant l'extrait de code suivant dans la console DevTools pour voir ce que vous pouvez trouver:

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