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
- Conocimientos de desarrollo en HTML y JavaScript
- Recomendación: Lee la documentación sobre la métrica INP de web.dev.
Qué aprenderás
- Cómo agregar la biblioteca de
web-vitalsa 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.
- Clona el repo en tu terminal:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git. - Navega al directorio clonado:
cd web-vitals-codelabs/measuring-inp. - Instala las dependencias:
npm ci. - Inicia el servidor web:
npm run start. - Visita http://localhost:8080/ en tu navegador.
Prueba la página
En este codelab, se usa Gastropodicon (un sitio popular de referencia sobre la anatomía de los caracoles) para explorar posibles problemas con el INP.

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 ellas en las pestañas que se encuentran en la parte superior de Herramientas para desarrolladores en cualquier momento.
- Los problemas de 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.

4. Cómo instalar web-vitals
web-vitals es una biblioteca de JavaScript para medir las métricas de Web Vitals 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á el script 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 Core Web Vitals en una carga de página.
- La compilación de "atribució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 a 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 de nuevo 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 volver 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.

5. ¿Qué incluye una atribución?
Comencemos con la primera interacción que tendrán la mayoría de los usuarios con la página: el cuadro de diálogo de consentimiento de uso 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 Sí para aceptar las cookies (de demostración) y consulta los datos del INP que ahora se registran en la consola de Herramientas para desarrolladores.

Esta información de nivel superior está disponible en las compilaciones de Web Vitals estándar y de atribución:
{
name: 'INP',
value: 344,
rating: 'needs-improvement',
entries: [...],
id: 'v4-1715732159298-8028729544485',
navigationType: 'reload',
attribution: {...},
}
El tiempo transcurrido 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 generar 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 los scripts 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,pointerupyclick). 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. En la guía para optimizar el INP, encontrarás 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 el INP, en este caso, un clic. La atribución processedEventEntries incluye 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 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 los scripts 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(deattribution.interactionTargety la propiedadinvokeren una entrada de atribución de secuencia de comandos). - La mayor parte del tiempo se dedicó a ejecutar los detectores de eventos (de
attribution.processingDurationen comparación con la métrica totalvalue). - El código del objeto de escucha de eventos lento comienza con un objeto de escucha de clics definido en
third-party/cmp.js(desdescripts.sourceURL).
Con esos datos, ya sabemos 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 uso 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á pasando?
Datos de atribución
Primero, una exploración 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) dedicado a ejecutar este objeto de escucha de eventos se dedicó en realidad a la agitación del diseño.
La solución de este problema debería ser una prioridad.
Usuarios repetidos
Individualmente, no hay nada especialmente destacable en las siguientes dos entradas de 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 uno tras otro. 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 caracteres.
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 otros detectores 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 se enfoca en 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 dedicó a la demora de entrada, con 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 otro elemento 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 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, 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), qué función fue la responsable exactamente 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 el retraso en la presentación el 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 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 encuentre el destino o los roles de ARIA que cumpla 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.
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.
Atribución 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 una secuencia de comandos 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);