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 el 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
  • Usa los datos de atribución para diagnosticar dónde y cómo comenzar a mejorar el INP.

Lo que necesitarás

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

2. Prepárate

Obtén y ejecuta el código

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

  1. Clona el repo en tu terminal: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git.
  2. Navega 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.

Probar la página

En este codelab, se usa Gastropodicon (un sitio de referencia popular 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 saber qué interacciones son lentas.

3. Cómo orientarse en las Herramientas para desarrolladores de Chrome

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

En este codelab, usaremos el panel Rendimiento y la Consola. Puedes cambiar entre ellos en las pestañas de la parte superior de DevTools en cualquier momento.

  • Los problemas del INP suelen ocurrir en dispositivos móviles, por lo que debes cambiar a la emulación de pantalla para dispositivos móviles.
  • Si realizas pruebas 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 en la parte superior derecha del panel Rendimiento y, luego, selecciona CPU 4x slowdown.

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

4. Instala web-vitals

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

Existen varias formas de agregar la biblioteca a una página. La forma en que instalarás la biblioteca en tu propio sitio dependerá de cómo administres las dependencias, el proceso de compilación y otros factores. Asegúrate de consultar la documentación de la biblioteca para conocer todas las opciones.

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:

  • La compilación "estándar" se debe usar si deseas hacer un seguimiento de los valores de las métricas de las Métricas web esenciales en una carga de página.
  • La compilación "attribution" 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 en la parte inferior 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 con la página nuevamente con la consola abierta. A medida que haces clic en la página, no se registra nada.

El INP se mide durante todo el ciclo de vida de una página, por lo que, de forma predeterminada, web-vitals no informa el INP hasta que el usuario abandona o cierra la página. Este es el comportamiento ideal para el balizamiento de elementos como las estadísticas, pero no es tan ideal para la depuración interactiva.

web-vitals proporciona una opción reportAllChanges para generar informes más detallados. Cuando está habilitada, no se informa cada interacción, pero sí se informa cada vez que hay una interacción más lenta que cualquier otra 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 registrarse en la consola, y se actualizarán cada vez que haya una nueva interacción más lenta. Por ejemplo, intenta escribir en el cuadro de búsqueda y, luego, borrar la entrada.

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

5. ¿Qué se incluye en una atribución?

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

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

Haz clic en para aceptar las cookies (de demostración) y consulta los datos del INP que ahora se registran en la consola de Herramientas para desarrolladores.

Objeto de datos del 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 de las métricas web vitales:

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

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

Sin embargo, para saber qué sucede durante este período, nos interesa más la propiedad attribution. Para compilar los datos de atribución, web-vitals busca qué cuadro de animación largo (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, un diseño y un estilo de requestAnimationFrame.

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

Primero, hay información sobre con qué 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 la sincronización de forma general:

  • inputDelay: Es el tiempo transcurrido entre el momento en que el usuario comenzó la interacción (por ejemplo, hizo clic con el mouse) y el momento en que comenzó a ejecutarse el objeto de escucha de eventos para esa interacción. En este caso, la demora de entrada fue de solo 27 milisegundos, incluso con la limitación de la CPU activada.
  • processingDuration: Es el tiempo que tardan los objetos de escucha de eventos en ejecutarse hasta completarse. 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 unirán en este tiempo. En este caso, la duración del procesamiento es de 295.6 milisegundos, la mayor parte del tiempo del INP.
  • presentationDelay: Es el tiempo transcurrido desde que se completaron los objetos de escucha de eventos hasta que el navegador terminó de pintar el siguiente fotograma. En este caso, 21.4 milisegundos.

Estas fases del INP pueden ser un indicador vital para diagnosticar qué se debe optimizar. La guía para optimizar el INP contiene más información sobre este tema.

Si profundizamos un poco más, el processedEventEntries contiene cinco eventos, a diferencia del único evento en el array entries del 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 de INP, en este caso, un clic. Los objetos de atribución processedEventEntries son todos los eventos que se procesaron durante el mismo fotograma. Ten en cuenta 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 la lentitud de la respuesta.

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

longAnimationFrameEntries

Expande 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 varios valores útiles, como el desglose de la cantidad de tiempo dedicado a aplicar el diseño. El artículo sobre la API de Long Animation Frames profundiza más en estas propiedades. Por el momento, nos interesa principalmente la propiedad scripts, que contiene entradas que proporcionan detalles sobre las secuencias de comandos responsables del fotograma 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 decir que el tiempo se dedicó principalmente a un solo event-listener, invocado en BUTTON#confirm.onclick. Incluso podemos ver la URL de la fuente del script y la posición del carácter en la que se definió la función.

Conclusión

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

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

Estos datos son suficientes para saber dónde debemos optimizar.

6. Varios objetos de escucha de eventos

Actualiza la página para que la consola de Herramientas para desarrolladores esté despejada y la interacción de consentimiento de cookies ya no sea la más larga.

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

Datos de atribución

Primero, un análisis general 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 bajo (con la 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 de scripts son más interesantes.

Hiperpaginación

La primera entrada del array scripts nos brinda 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 de la duración del procesamiento se produce durante la ejecución de este script, que es un objeto de escucha input (el invocador es INPUT#search-terms.oninput). Se proporciona el nombre de la función (handleSearch), así como la posición del carácter dentro del archivo fuente index.js.

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

Esta debería ser una prioridad principal para corregir.

Objetos de escucha repetidos

Individualmente, no hay nada especialmente destacable en las próximas 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 de keyup que se ejecutan una tras otra. Los objetos de escucha son funciones anónimas (por lo que no se informa nada en la propiedad sourceFunctionName), pero aún tenemos un archivo fuente y una posición de caracteres, por lo que podemos encontrar dónde está el código.

Lo extraño es que ambos provienen del mismo archivo fuente y posición de carácter.

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

Este efecto también puede agravarse, ya que cuanto más tarden en completarse los objetos de escucha de eventos, más eventos de entrada adicionales pueden llegar, lo que extiende la interacción lenta por mucho más tiempo.

Dado que se trata de una interacción de búsqueda o autocompletar, una buena estrategia sería eliminar el rebote de la entrada para que, como máximo, se procese una pulsación de tecla por fotograma.

7. Retraso de entrada

El motivo habitual de las demoras en la entrada (el tiempo que transcurre desde que el usuario interactúa hasta que un objeto de escucha de eventos puede comenzar a procesar la interacción) es que el subproceso principal está ocupado. Esto podría deberse a varios motivos:

  • 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, 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: si haces clic en el logotipo de caracol en la parte superior de la página, comenzará a animarse y a realizar 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 está 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 qué tan alto puedes activar el INP.

Por ejemplo, incluso si no activas ningún otro objeto de escucha de eventos (como hacer clic en el cuadro de búsqueda y enfocarlo justo cuando rebota el caracol), el trabajo del subproceso principal hará que la página no responda durante un tiempo 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 solo enfoca el cuadro de búsqueda durante el rebote del 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, con una duración de procesamiento de 4.9 milisegundos, y la mayor parte de la interacción deficiente se produjo en la demora de entrada, que tardó 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 lo hizo, también sabemos que esa parte de la interacción se completó rápidamente y no fue un problema. En cambio, había algo más en la página que retrasaba el inicio del procesamiento de la interacción, pero ¿cómo sabríamos por dónde empezar a buscar?

Las entradas de secuencias 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, sí ralentizó el fotograma de la animación, por lo que se incluye en los datos de LoAF que se unen 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 la responsable y dónde se ubicaba en nuestros archivos fuente.

8. Retraso en la presentación: Cuando una actualización no se renderiza

La demora en la presentación mide el tiempo transcurrido desde que los objetos de escucha de eventos terminaron de ejecutarse hasta que el navegador puede pintar un nuevo fotograma en la pantalla, lo que muestra comentarios visibles para el usuario.

Actualiza la página para restablecer el valor del INP y, luego, abre el menú de hamburguesas. Hay un enganche definido 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 constituye la mayor parte de la interacción lenta. Esto significa que lo que sea que esté bloqueando el subproceso principal ocurre después de 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 única entrada del array scripts, vemos que el tiempo se dedica a un 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

Agregación de datos de campo

Vale la pena reconocer que todo esto es más fácil cuando se observa una sola entrada de atribución del INP de una sola carga de página. ¿Cómo se pueden agregar estos datos para depurar el INP en función de los datos de campo? La cantidad de detalles útiles en realidad dificulta más la tarea.

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 tiene nombres de clases de CSS compilados que cambian de una compilación a otra, los selectores web-vitals del mismo elemento pueden ser diferentes en las distintas compilaciones.

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

Del mismo modo, las entradas de scripts pueden tener hashes basados en archivos en sus rutas de sourceURL que dificultan su combinación, pero podrías quitar los hashes según tu proceso de compilación conocido antes de enviar los datos de vuelta a través de balizas.

Lamentablemente, no hay una ruta sencilla con datos tan complejos, pero incluso usar un subconjunto de ellos es más valioso que no tener datos de atribución para el proceso de depuración.

La atribución está en todas partes.

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

Ya puedes usar los datos de atribución del 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 lo que puedes encontrar:

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