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

1. Introduction

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

Prérequis

Objectifs de l'atelier

  • Comment 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 l'INP.

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/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 le 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 Gastropodicon

Essayez d'interagir avec la page pour identifier les interactions lentes.

3. Se repérer dans 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.

Dans cet atelier de programmation, nous allons utiliser à 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 des outils de développement.

  • Les problèmes d'INP se produisent le plus souvent sur les appareils mobiles. Passez donc à l'émulation d'affichage mobile.
  • Si vous effectuez des tests sur un ordinateur de bureau ou portable, les performances seront probablement bien meilleures que sur un véritable appareil mobile. Pour obtenir une vision plus réaliste des performances, cliquez sur l'icône en forme d'engrenage en haut à droite du panneau Performances, puis sélectionnez Ralentissement du processeur x4.

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

4. Installer web-vitals

web-vitals est une bibliothèque JavaScript permettant de mesurer les métriques des signaux Web que vos utilisateurs rencontrent. Vous pouvez utiliser la bibliothèque pour capturer ces valeurs, puis les envoyer à un point de terminaison Analytics pour une analyse ultérieure. Pour notre cas d'utilisation, il s'agit de déterminer quand et où les interactions lentes se produisent.

Il existe plusieurs façons d'ajouter la bibliothèque à une page. La façon dont vous installerez la bibliothèque sur votre propre site dépendra de la façon dont vous gérez les dépendances, du processus de compilation et d'autres facteurs. Consultez la documentation de la bibliothèque pour découvrir toutes les options disponibles.

Cet atelier de programmation installera le script à partir de npm et le chargera 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 finit par avoir la valeur qu'elle a.

Pour mesurer l'INP dans cet atelier de programmation, nous avons besoin de la version avec 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 enregistrez 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 laissant la console 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 conséquent, par défaut, web-vitals ne le signale que lorsque l'utilisateur quitte ou ferme la page. C'est le comportement idéal pour le beaconing dans le cadre d'analyses, mais moins adapté au débogage interactif.

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

Essayez d'ajouter l'option au script et d'interagir de 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 désormais être signalées à la console et mises à jour chaque fois qu'une nouvelle interaction la plus lente est détectée. Par exemple, essayez de saisir du texte dans le champ de recherche, puis de le supprimer.

Capture d&#39;écran de la console d&#39;outils de développement avec les messages INP 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 nombreuses pages comportent des scripts qui nécessitent que les cookies soient déclenchés de manière synchrone lorsqu'un 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 qui sont maintenant enregistrées dans la console des outils de développement.

Objet de données INP consigné dans la console des outils pour les développeurs

Ces informations de premier niveau sont disponibles dans les versions standard et d'attribution des Web Vitals :

{
  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 le prochain affichage était de 344 millisecondes, ce qui correspond à une INP "Nécessite des améliorations". Le tableau entries contient toutes les valeurs PerformanceEntry associées à cette interaction (dans ce cas, un seul événement de clic).

Toutefois, pour savoir ce qui se passe pendant cette période, nous nous intéressons surtout à la propriété attribution. Pour créer les données d'attribution, web-vitals recherche les Long Animations Frame (LoAF) qui se chevauchent avec l'événement de clic. Le LoAF peut ensuite fournir des données détaillées sur la façon dont le temps a été passé pendant ce frame, des scripts qui ont été exécutés au temps passé dans un rappel requestAnimationFrame, au style et à la 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, 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.

Voici une vue d'ensemble du calendrier :

  • inputDelay : temps écoulé entre le moment où l'utilisateur a commencé l'interaction (par exemple, en cliquant avec la souris) et le moment où l'écouteur d'événements pour cette interaction a commencé à s'exécuter. 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 complète des lecteurs d'événements. Souvent, les pages comportent 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 regroupés dans ce délai. Dans ce cas, la durée de traitement est de 295,6 millisecondes, ce qui représente la majeure partie du temps INP.
  • presentationDelay : temps écoulé entre la fin des écouteurs d'événements et le moment où le navigateur a fini de peindre le frame suivant. Dans ce cas, 21,4 millisecondes.

Ces phases 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 y regardant de plus près, processedEventEntries contient cinq événements, contrairement à l'événement unique du tableau INP de premier niveau entries. 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 the INP, qui est un clic dans ce cas. L'attribution processedEventEntries correspond à tous les é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. Il peut être essentiel de connaître ces autres événements s'ils étaient également lents, car ils ont tous contribué à la lenteur de la réactivité.

Enfin, il y a le tableau longAnimationFrameEntries. Il peut s'agir d'une seule entrée, mais il existe des cas où une interaction peut s'étendre sur plusieurs frames. Voici le cas le plus simple avec un seul frame d'animation de longue durée.

longAnimationFrameEntries

Développez 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: [{...}]
}],

Vous y trouverez plusieurs valeurs utiles, comme la durée passée à appliquer des styles. L'article sur l'API Long Animation Frames fournit plus de détails sur ces propriétés. Pour le moment, nous nous intéressons principalement à la propriété scripts, qui contient des entrées fournissant 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 du caractère où la fonction a été définie.

Plats à emporter

Que peut-on 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).
  • La majeure partie du temps a été consacrée à l'exécution des écouteurs d'événements (à partir de attribution.processingDuration par rapport à la métrique totale value).
  • Le code de l'écouteur d'événements lents commence par un écouteur de clics défini dans third-party/cmp.js (à partir de scripts.sourceURL).

Nous disposons de suffisamment de données pour savoir où nous devons optimiser nos campagnes.

6. Plusieurs écouteurs d'événements

Actualisez la page pour que la console DevTools soit vide 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 à votre avis ?

Données d'attribution

Voici un aperçu général d'un exemple de test de la démo :

{
  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 la limitation du processeur activée) provenant d'une interaction au clavier avec l'élément input#search-terms. La majorité du temps (1 061 millisecondes sur un total de 1 072 millisecondes d'INP) a été consacrée à la durée de traitement.

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

Thrashing de 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'invocateur 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.

Cependant, une nouvelle propriété est disponible : forcedStyleAndLayoutDuration. Il s'agit du temps passé dans cette invocation de script où le navigateur a été forcé de réorganiser la page. En d'autres termes, 78 % du temps (388 millisecondes sur 497) passé à exécuter cet écouteur d'événement a en fait été consacré au thrashing de mise en page.

Il s'agit d'un problème à résoudre en priorité.

Auditeurs réguliers

Individuellement, les deux prochaines entrées de script 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'un après l'autre. Les écouteurs sont des fonctions anonymes (par conséquent, rien n'est indiqué dans la propriété sourceFunctionName), mais nous disposons toujours d'un fichier source et d'une position de caractère. Nous pouvons donc 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 frappes sur une touche dans un seul frame d'animation, ce qui a entraîné l'exécution de cet écouteur d'événements deux fois avant que quoi que ce soit puisse être affiché.

Cet effet peut également se cumuler : plus les écouteurs d'événements mettent de temps à se terminer, plus des événements d'entrée supplémentaires peuvent arriver, ce qui prolonge d'autant l'interaction lente.

Comme il s'agit d'une interaction de recherche/saisie semi-automatique, il est judicieux de débouncer l'entrée afin qu'au maximum une pression sur une touche soit traitée par frame.

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

La raison typique des délais d'entrée (le temps qui s'écoule entre le moment où l'utilisateur interagit et le moment où un écouteur d'événements peut commencer à traiter l'interaction) est que le thread principal est occupé. Plusieurs raisons peuvent expliquer ce problème :

  • La page se charge et le thread principal est occupé à effectuer le travail initial 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 on l'a vu dans le dernier exemple.

La page de démonstration comporte une fonctionnalité secrète : si vous cliquez sur le logo en forme d'escargot en haut de la page, il commence à s'animer et à effectuer un travail JavaScript important sur le thread principal.

  • Cliquez sur le logo en forme d'escargot pour lancer l'animation.
  • Les tâches JavaScript sont déclenchées lorsque l'escargot se trouve en bas du rebond. Essayez d'interagir avec la page le plus près possible du bas du rebond et voyez quel 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 la zone de recherche et en la sélectionnant juste au moment où l'escargot rebondit), le travail du thread principal entraînera l'absence de réponse de la page pendant une durée notable.

Sur de nombreuses pages, le travail intensif du thread principal ne se comporte pas aussi bien. Toutefois, il s'agit d'une bonne démonstration pour voir comment il peut être identifiable dans les données d'attribution de l'INP.

Voici un exemple d'attribution qui ne se concentre que sur le champ de recherche lors du rebond de l'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 se sont exécutés rapidement, affichant une durée de traitement de 4,9 millisecondes. La grande majorité de la mauvaise interaction a été consacrée au délai d'entrée, qui a duré 702,3 millisecondes sur un total de 728.

Il peut être difficile de déboguer cette situation. Même si nous savons avec quoi et comment l'utilisateur a interagi, nous savons également que cette partie de l'interaction s'est déroulée rapidement et sans 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 où commencer à chercher ?

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 jointes à 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 qui s'écoule entre la fin de l'exécution des écouteurs d'événements et le moment où le navigateur est en mesure d'afficher un nouvel frame à l'écran, ce qui permet à l'utilisateur de voir un retour visuel.

Actualisez la page pour réinitialiser à nouveau la valeur 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 majorité de l'interaction lente. Cela signifie que tout 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 constatons 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éger les données de champ

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 de champ ? La quantité de détails utiles rend en fait la tâche plus difficile.

Par exemple, il est extrêmement utile de savoir quel élément de page est une source fréquente d'interactions lentes. Toutefois, si votre page comporte 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'une compilation à l'autre.

Vous devez plutôt réfléchir à votre application en particulier 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 par balise, vous pouvez remplacer le sélecteur web-vitals par un identifiant de votre choix, basé sur le composant dans lequel se trouve la cible ou sur les rôles ARIA que la cible remplit.

De même, les entrées scripts peuvent comporter 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 méthode simple pour gérer des données aussi complexes. Toutefois, même si vous n'en utilisez qu'une partie, cela sera plus utile pour le processus de débogage que de ne pas avoir de données d'attribution du tout.

L'attribution partout !

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

Vous pouvez maintenant 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