Medición de la interacción con el siguiente procesamiento de imagen (INP)

1. Introducción

Este es un codelab interactivo para aprender a medir la Interaction to Next Paint (INP) con la biblioteca web-vitals.

Requisitos previos

Qué aprenderás

  • Cómo agregar la biblioteca de web-vitals a tu página y usar sus datos de atribución
  • Utiliza los datos de atribución para diagnosticar dónde y cómo comenzar a mejorar INP.

Lo que necesitarás

  • Una computadora con la capacidad de clonar código desde GitHub y ejecutar comandos npm.
  • Un editor de texto
  • Una versión reciente de Chrome para que funcionen todas las mediciones de interacción.

2. Prepárate

Cómo obtener y ejecutar el código

El código se encuentra en el repositorio de web-vitals-codelabs.

  1. Clona el repositorio en tu terminal: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git.
  2. Pasa al directorio clonado: cd web-vitals-codelabs/measuring-inp.
  3. Instala las dependencias: npm ci.
  4. Inicia el servidor web: npm run start.
  5. Visita http://localhost:8080/ en tu navegador.

Prueba la página

En este codelab, se usa Gastropodicon (un popular sitio de referencia sobre la anatomía de los caracoles) para explorar posibles problemas con el INP.

Captura de pantalla de la página de demostración de Gastropodicon

Intenta interactuar con la página para tener una idea de qué interacciones son lentas.

3. Cómo comenzar a usar las Herramientas para desarrolladores de Chrome

Abre las Herramientas para desarrolladores desde Más herramientas > En el menú Herramientas para desarrolladores, haz clic con el botón derecho en la página y selecciona Inspeccionar, o bien usa una combinación de teclas.

En este codelab, usaremos el panel Rendimiento y Console. Puedes alternar entre estas opciones en cualquier momento en las pestañas de la parte superior de Herramientas para desarrolladores.

  • Los problemas de INP ocurren con mayor frecuencia en los dispositivos móviles, por lo que debes cambiar a la emulación de pantalla para dispositivos móviles.
  • Si realizas la prueba en una computadora de escritorio o laptop, es probable que el rendimiento sea significativamente mejor que en un dispositivo móvil real. Para obtener una vista más realista del rendimiento, presiona el ícono de ajustes ubicado en la parte superior derecha del panel Rendimiento y selecciona Disminución de 4 veces la velocidad de la CPU.

Captura de pantalla del panel Performance de Herramientas para desarrolladores junto a la app, con la opción de ralentización de CPU 4x seleccionada

4. Instalación web-vitals

web-vitals es una biblioteca de JavaScript para medir las métricas de las Métricas web que experimentan los usuarios. Puedes usar la biblioteca para capturar esos valores y, luego, redireccionarlos a un extremo de análisis para su posterior análisis, con el objetivo de que podamos averiguar cuándo y dónde ocurren interacciones lentas.

Existen algunas formas diferentes de agregar la biblioteca a una página. La forma de instalar la biblioteca en tu propio sitio dependerá de cómo administres las dependencias, el proceso de compilación y otros factores. Asegúrate de revisar los documentos de la biblioteca para ver todas las opciones disponibles.

Este codelab se instalará desde npm y cargará la secuencia de comandos directamente para evitar profundizar en un proceso de compilación en particular.

Puedes usar dos versiones de web-vitals:

  • El término "estándar" se debe usar si quieres hacer un seguimiento de los valores de las métricas de las Métricas web esenciales en la carga de una página.
  • La "atribución" La compilación agrega información de depuración adicional a cada métrica para diagnosticar por qué una métrica termina con el valor que tiene.

Para medir el INP en este codelab, queremos la compilación de atribución.

Ejecuta npm install -D web-vitals para agregar web-vitals al devDependencies del proyecto.

Agrega web-vitals a la página:

Agrega la versión de atribución de la secuencia de comandos al final de index.html y registra los resultados en la consola:

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

  onINP(console.log);
</script>

Probar

Intenta interactuar de nuevo con la página con la consola abierta. Cuando haces clic en la página, no se registra nada.

El INP se mide durante todo el ciclo de vida de una página y, por lo tanto, de forma predeterminada, web-vitals no informa el INP hasta que el usuario abandona la página o la cierra. Este es el comportamiento ideal para el contador de algo como el análisis, pero es menos ideal para la depuración interactiva.

web-vitals proporciona una opción reportAllChanges para informes más detallados. Cuando se habilita, no se informa todas las interacciones, pero se informa cada vez que hay una interacción más lenta que cualquiera anterior.

Intenta agregar la opción a la secuencia de comandos y vuelve a interactuar con la página:

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

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

Actualiza la página, y las interacciones ahora deberían informarse a la consola y se deben actualizar cada vez que haya una nueva, la más lenta. Por ejemplo, intenta escribir en el cuadro de búsqueda y, luego, borra la entrada.

Captura de pantalla de la consola de Herramientas para desarrolladores con los mensajes INP impresos correctamente

5. ¿Qué contiene una atribución?

Comencemos con la primera interacción que la mayoría de los usuarios tendrán con la página: el cuadro de diálogo de consentimiento de uso de cookies.

Muchas páginas tienen secuencias de comandos que necesitan que se activen cookies de forma síncrona cuando un usuario acepta las cookies, lo que provoca que el clic se convierta en una interacción lenta. Eso es lo que sucede aquí.

Haz clic en Yes para aceptar las cookies (de demostración) y observa los datos de INP que ahora están registrados en la consola de Herramientas para desarrolladores.

Objeto de datos de INP registrado en la consola de Herramientas para desarrolladores

Esta información de nivel superior está disponible en las compilaciones estándar y de atribución web-vitals:

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

El período desde que el usuario hizo clic hasta la siguiente pintura fue de 344 milisegundos, lo que significa que "necesita mejorar". INP El array entries tiene todos los valores PerformanceEntry asociados con esta interacción; en este caso, solo un evento de clic.

Sin embargo, para averiguar qué sucede durante este tiempo, lo que más nos interesa es la propiedad attribution. Para compilar los datos de atribución, web-vitals encuentra qué Marco de animaciones largas (LoAF) se superpone con el evento de clic. Luego, el LoAF puede proporcionar datos detallados sobre cómo se empleó el tiempo durante ese fotograma, desde las secuencias de comandos que se ejecutaron hasta el tiempo dedicado a una devolución de llamada a requestAnimationFrame, el estilo y el diseño.

Expande la propiedad attribution para ver más información. Los datos son mucho más completos.

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

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

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

En primer lugar, hay información sobre el contenido con el que se interactuó:

  • interactionTargetElement: Es una referencia activa al elemento con el que se interactuó (si el elemento no se quitó del DOM).
  • interactionTarget: Es un selector para encontrar el elemento dentro de la página.

A continuación, se desglosa el tiempo de manera general:

  • inputDelay: Es el tiempo desde que el usuario inició la interacción (por ejemplo, hizo clic con el mouse) y el momento en que comenzó a ejecutarse el objeto de escucha de eventos de esa interacción. En este caso, el retraso de entrada fue de solo 27 milisegundos, incluso con la limitación de la CPU activada.
  • processingDuration: Es el tiempo que tarda los objetos de escucha de eventos en ejecutarse hasta su finalización. A menudo, las páginas tendrán varios objetos de escucha para un solo evento (por ejemplo, pointerdown, pointerup y click). Si todos se ejecutan en el mismo fotograma de animación, se fusionarán en este tiempo. En este caso, la duración del procesamiento tarda 295.6 milisegundos, que es la mayor parte del tiempo de INP.
  • presentationDelay: Es el tiempo que transcurre desde que se completan los objetos de escucha de eventos hasta que el navegador termina de pintar el siguiente fotograma. En este caso, 21.4 milisegundos.

Estas fases INP pueden ser una señal vital para diagnosticar lo que debe optimizarse. En la guía para optimizar el INP, encontrarás más información sobre este tema.

Si profundizamos un poco más, processedEventEntries contiene cinco eventos, a diferencia del único evento del array de entries de INP de nivel superior. ¿Cuál es la diferencia?

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', ...},
],

La entrada de nivel superior es el evento INP, en este caso, un clic. Los processedEventEntries de atribución son todos los eventos que se procesaron durante el mismo fotograma. Observa que incluye otros eventos, como mouseover y mousedown, no solo el evento de clic. Conocer estos otros eventos puede ser fundamental si también fueron lentos, ya que todos contribuyeron a que la capacidad de respuesta fuera lenta.

Por último, está el array longAnimationFrameEntries. Puede ser una sola entrada, pero hay casos en los que una interacción puede propagarse en varios fotogramas. Este es el caso más simple, con un único fotograma de animación largo.

longAnimationFrameEntries

Expansión de la entrada de LoAF:

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

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

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

Aquí hay una serie de valores útiles, como desglosar la cantidad de tiempo dedicado al diseño. En el artículo sobre la API de Long Animation Frames, se explica con más detalle estas propiedades. En este momento, nos interesa principalmente la propiedad scripts, que contiene entradas que proporcionan detalles sobre las secuencias de comandos responsables del marco de larga duración:

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
}]

En este caso, podemos detectar que el tiempo se gastó principalmente en un solo elemento event-listener, invocado en BUTTON#confirm.onclick. Incluso podemos ver la URL de origen de la secuencia de comandos y la posición de los caracteres donde se definió la función.

Comida para llevar

¿Qué se puede determinar sobre este caso a partir de estos datos de atribución?

  • La interacción se activó con un clic en el elemento button#confirm (desde attribution.interactionTarget y la propiedad invoker en una entrada de atribución de secuencia de comandos).
  • El tiempo se dedicó, principalmente, a la ejecución de objetos de escucha de eventos (del attribution.processingDuration en comparación con la métrica total de value).
  • El código del objeto de escucha de eventos lentos se inicia en un objeto de escucha de clics definido en third-party/cmp.js (desde scripts.sourceURL).

Son suficientes datos para saber dónde hay que optimizar las campañas.

6. Varios objetos de escucha de eventos

Actualiza la página para que la consola de Herramientas para desarrolladores sea clara y la interacción de consentimiento de cookies ya no sea la interacción más prolongada.

Comienza a escribir en el cuadro de búsqueda. ¿Qué muestran los datos de atribución? ¿Qué crees que está pasando?

Datos de atribución

Primero, se muestra un análisis de alto nivel de un ejemplo de prueba de la demostración:

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

Es un valor de INP deficiente (con limitación de CPU habilitada) de una interacción del teclado con el elemento input#search-terms. La mayor parte del tiempo (1,061 milisegundos de un INP total de 1,072 milisegundos) se dedicó a la duración del procesamiento.

Sin embargo, las entradas scripts son más interesantes.

Patrulla de diseños

La primera entrada del array scripts nos proporciona un contexto valioso:

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 mayor parte del tiempo del procesamiento ocurre durante la ejecución de esta secuencia de comandos, que es un objeto de escucha input (el invocador es INPUT#search-terms.oninput). Se proporciona el nombre de la función (handleSearch), al igual que la posición de los caracteres dentro del archivo de origen index.js.

Sin embargo, hay una propiedad nueva: forcedStyleAndLayoutDuration. Este fue el tiempo dedicado a esta invocación de secuencia de comandos durante el cual el navegador se vio obligado a rediseñar la página. En otras palabras, el 78% del tiempo (388 milisegundos de 497) dedicado a ejecutar este objeto de escucha de eventos en realidad se dedicó a la hiperpaginación de diseños.

Esta debe ser una prioridad para corregir.

Objetos de escucha repetidos

De manera individual, no hay nada extraordinario en las siguientes dos entradas de la secuencia de comandos:

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
}]

Ambas entradas son objetos de escucha keyup y se ejecutan una inmediatamente después de la otra. Los objetos de escucha son funciones anónimas (por lo tanto, no se informa nada en la propiedad sourceFunctionName), pero seguimos teniendo un archivo fuente y la posición de los caracteres para poder encontrar dónde está el código.

Lo extraño es que ambos pertenezcan al mismo archivo fuente y a la misma posición del carácter.

El navegador terminó procesando varias pulsaciones de teclas en un solo fotograma de animación, lo que hizo que este objeto de escucha de eventos se ejecutara dos veces antes de que se pudiera pintar algo.

Este efecto también puede acumularse: cuanto más tiempo tarden en completarse los objetos de escucha de eventos, más eventos de entrada adicionales podrán ingresar, lo que extiende la interacción lenta durante mucho más tiempo.

Como se trata de una interacción de búsqueda/autocompletado, eliminar la entrada sería una buena estrategia para que, como máximo, se procese una pulsación de tecla por fotograma.

7. Retraso de entrada

El motivo típico de los retrasos de entrada (el tiempo desde que el usuario interactúa hasta que un objeto de escucha de eventos puede comenzar a procesar la interacción) es porque el subproceso principal está ocupado. Esto puede deberse a varias causas:

  • La página se está cargando y el subproceso principal está ocupado realizando el trabajo inicial de configurar el DOM, diseñar y aplicar estilos a la página, y evaluar y ejecutar secuencias de comandos.
  • La página suele estar ocupada; por ejemplo, cuando se están ejecutando cálculos, animaciones basadas en secuencias de comandos o anuncios.
  • Las interacciones anteriores tardan tanto en procesarse que retrasan las interacciones futuras, como se vio en el último ejemplo.

La página de demostración tiene una función secreta que, si haces clic en el logotipo de la parte superior de la página, comenzará a animarse y hará un trabajo pesado de JavaScript en el subproceso principal.

  • Haz clic en el logotipo del caracol para iniciar la animación.
  • Las tareas de JavaScript se activan cuando el caracol se encuentra en la parte inferior del rebote. Intenta interactuar con la página lo más cerca posible de la parte inferior del rebote y observa la altura de un INP que puedes activar.

Por ejemplo, incluso si no activas ningún otro objeto de escucha de eventos, como hacer clic y enfocar el cuadro de búsqueda justo cuando rebota el caracol, el trabajo del subproceso principal hará que la página no responda durante un período considerable.

En muchas páginas, el trabajo pesado del subproceso principal no se comportará tan bien, pero esta es una buena demostración para ver cómo se puede identificar en los datos de atribución del INP.

A continuación, se muestra un ejemplo de atribución que consiste en enfocar solo el cuadro de búsqueda durante el rebote de un caracol:

{
  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: [{...}]
    }]
  }
}

Como se predijo, los objetos de escucha de eventos se ejecutaron rápidamente, lo que muestra una duración de procesamiento de 4.9 milisegundos y que la gran mayoría de la interacción deficiente se llevó a cabo en el retraso de entrada, lo que llevó a 702.3 milisegundos de un total de 728.

Esta situación puede ser difícil de depurar. Aunque sabemos con qué interactuó el usuario y cómo, también sabemos que esa parte de la interacción se completó rápidamente y no fue un problema. En cambio, fue otra cosa en la página que retrasó la interacción desde el inicio del procesamiento, pero ¿cómo sabríamos por dónde empezar a buscar?

Las entradas de la secuencia de comandos de LoAF están aquí para salvar el día:

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
}]

Aunque esta función no tenía nada que ver con la interacción, retrasó el fotograma de la animación, por lo que se incluye en los datos de LoAF unidos con el evento de interacción.

A partir de esto, podemos ver cómo se activó la función que retrasó el procesamiento de la interacción (mediante un objeto de escucha animationiteration), exactamente qué función fue responsable y dónde se encontraba en nuestros archivos fuente.

8. Retraso en la presentación: cuando una actualización simplemente no funciona.

El retraso en la presentación mide el tiempo que transcurre desde que los objetos de escucha de eventos terminan de ejecutarse hasta que el navegador puede pintar un nuevo fotograma en la pantalla, lo que le muestra al usuario comentarios visibles.

Actualiza la página para restablecer el valor de INP nuevamente y, luego, abre el menú de opciones. No hay duda de que hay un problema cuando se abre.

¿Cómo se ve esto?

{
  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: [{...}]
    }]
  }
}

Esta vez es la demora en la presentación la que conforma la mayor parte de la interacción lenta. Es decir, todo lo que bloquee el subproceso principal se produce una vez que se completan los objetos de escucha de eventos.

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,
}]

Si observamos la entrada única del array scripts, vemos que el tiempo se dedicó a una user-callback de un FrameRequestCallback. Esta vez, la demora en la presentación se debe a una devolución de llamada requestAnimationFrame.

9. Conclusión

Agrega datos de campo

Vale la pena reconocer que esto es más fácil cuando se observa una sola entrada de atribución de INP desde una sola carga de página. ¿Cómo se pueden agregar estos datos para depurar INP según los datos del campo? La cantidad de detalles útiles hace que esto sea más difícil.

Por ejemplo, es muy útil saber qué elemento de la página es una fuente común de interacciones lentas. Sin embargo, si tu página compiló nombres de clases CSS que cambian de una compilación a otra, los selectores web-vitals del mismo elemento pueden ser diferentes entre las compilaciones.

En cambio, debes pensar en tu aplicación particular para determinar qué es más útil y cómo se pueden agregar los datos. Por ejemplo, antes de volver a enviar datos de atribución del contador, puedes reemplazar el selector web-vitals por un identificador propio, según el componente en el que se encuentre el objetivo, o los roles de ARIA que cumpla el objetivo.

De manera similar, las entradas scripts pueden tener hashes basados en archivos en sus rutas de acceso sourceURL que las hacen difíciles de combinar, pero puedes quitar los hash según tu proceso de compilación conocido antes de volver a enviar el contador de datos.

Lamentablemente, no existe una ruta fácil con datos tan complejos, pero incluso usar un subconjunto de ellos es más valioso que no tener ningún dato de atribución para el proceso de depuración.

Atribución en todas partes

La atribución INP basada en LoAF es una potente ayuda para la depuración. Ofrece datos detallados sobre lo que sucedió específicamente durante un INP. En muchos casos, puede dirigirte a la ubicación precisa en una secuencia de comandos en la que debes comenzar tus esfuerzos de optimización.

Ya puedes usar los datos de atribución de INP en cualquier sitio.

Incluso si no tienes acceso para editar una página, puedes recrear el proceso de este codelab ejecutando el siguiente fragmento en la consola de Herramientas para desarrolladores para ver qué encuentras:

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

Más información