1. Introducción
Este es un codelab interactivo para aprender a medir Interaction to Next Paint (INP) con la biblioteca web-vitals.
Requisitos previos
- Conocimientos de desarrollo de HTML y JavaScript
- Recomendación: Lee la documentación de la métrica INP de web.dev.
Qué aprenderás
- Cómo agregar la biblioteca
web-vitalsa tu página y usar sus datos de atribución - Cómo usar los datos de atribución para diagnosticar dónde y cómo comenzar a mejorar la INP
Lo que necesitarás
- Una computadora con la capacidad de clonar código de 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
Obtén y ejecuta el código
El código se encuentra en el el web-vitals-codelabs repositorio.
- Clona el repositorio en tu terminal:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git. - Desplázate 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 de anatomía de caracoles) para explorar posibles problemas con la INP.

Intenta interactuar con la página para tener una idea de 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 las 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 mucho 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. Instala web-vitals
web-vitals es una biblioteca de JavaScript para medir las métricas de Métricas web que experimentan tus usuarios. Puedes usar la biblioteca para capturar esos valores y, luego, enviarlos a un extremo de estadísticas para su análisis posterior, con el objetivo de averiguar cuándo y dónde se producen interacciones lentas.
Hay diferentes maneras 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.
En este codelab, se instalará desde npm y se cargará la secuencia de comandos directamente para evitar profundizar en un proceso de compilación en particular.
Hay dos versiones de web-vitals que puedes usar:
- Se debe usar la compilación "estándar" si deseas hacer un seguimiento de los valores de las métricas de 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 la INP en este codelab, queremos la compilación de atribución.
Ejecuta npm install -D web-vitals para agregar web-vitals a 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. Cuando haces clic en la página, no se registra nada.
La INP se mide durante todo el ciclo de vida de una página, por lo que, de forma predeterminada, web-vitals no informa la INP hasta que el usuario abandona o cierra la página. Este es el comportamiento ideal para enviar datos de baliza para algo como estadísticas, pero es menos ideal para depurar de forma interactiva.
web-vitals proporciona la opción reportAllChanges para generar informes más detallados. Cuando está habilitada, no se informa cada interacción, pero sí 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 informarse a la consola, y se actualizarán cada vez que haya una nueva más lenta. Por ejemplo, intenta escribir en el cuadro de búsqueda y, luego, borrar la entrada.

5. ¿Qué hay 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 se activen las cookies 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 (demostración) y echa un vistazo a los datos de INP que ahora se registran 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 web-vitals:
{
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 la siguiente pintura fue de 344 milisegundos, una "INP que necesita mejorar". 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, nos interesa más la propiedad attribution. Para compilar los datos de atribución, web-vitals busca qué Long Animations Frame (LoAF) se superpone con el evento de clic. Luego, el LoAF puede proporcionar datos detallados sobre cómo se invirtió el tiempo durante ese fotograma, desde las secuencias de comandos que se ejecutaron hasta el tiempo invertido en una devolución de llamada 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: [...],
}
Primero, hay información sobre con qué se interactuó:
interactionTargetElement: Una referencia activa al elemento con el que se interactuó (si el elemento no se quitó del DOM).interactionTarget: Un selector para encontrar el elemento dentro de la página.
A continuación, el tiempo se desglosa de manera general:
inputDelay: El tiempo transcurrido desde que el usuario inició la interacción (por ejemplo, hizo clic con el mouse) hasta que comenzó a ejecutarse el objeto de escucha de eventos para esa interacción. En este caso, el retraso de entrada fue de solo 27 milisegundos, incluso con la limitación de CPU activada.processingDuration: El tiempo que tardan en ejecutarse los objetos de escucha de eventos 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 tarda 295.6 milisegundos, la mayor parte del tiempo de INP.presentationDelay: 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 de INP pueden ser un indicador vital para diagnosticar lo que se debe optimizar. La guía Optimiza la INP tiene más información sobre este tema.
Si profundizamos un poco más, processedEventEntries contiene cinco eventos, en lugar del evento único en el array 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 de INP, en este caso, un clic. processedEventEntries de atribución 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 vital si también fueron lentos, ya que todos contribuyeron a una respuesta lenta.
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 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 la división de la cantidad de tiempo invertido en el estilo. El artículo de la API de Long Animation Frames profundiza en estas propiedades. Por ahora, 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 invirtió principalmente en un solo event-listener, invocado en BUTTON#confirm.onclick. Incluso podemos ver la URL de origen de la secuencia de comandos 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 estos 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). - El tiempo se invirtió principalmente en la ejecución de objetos de escucha de eventos (de
attribution.processingDurationen comparación con elvaluetotal de la métrica). - El código lento del objeto de escucha de eventos comienza con un objeto de escucha de clics definido en
third-party/cmp.js(descripts.sourceURL).
Son suficientes datos 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é clara y la interacción de consentimiento de cookies ya no sea la interacción 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 de teclado con el elemento input#search-terms. La mayor parte del tiempo (1,061 milisegundos de un total de 1,072 milisegundos de INP) se invirtió en la duración del procesamiento.
Sin embargo, las entradas scripts son más interesantes.
Diseño inestable
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 de la duración 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 del carácter dentro del archivo fuente index.js.
Sin embargo, hay una propiedad nueva: forcedStyleAndLayoutDuration. Este fue el tiempo invertido en 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) invertido en la ejecución de este objeto de escucha de eventos se invirtió en realidad en el diseño inestable.
Esta debe ser una prioridad máxima para corregir.
Objetos de escucha repetidos
Individualmente, no hay nada especialmente notable sobre 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 keyup, que se ejecutan una tras otra. Los objetos de escucha son funciones anónimas (por lo tanto, no se informa nada en la propiedad sourceFunctionName), pero aún tenemos un archivo fuente y una posición de carácter, 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 combinarse, ya que cuanto más tardan en completarse los objetos de escucha de eventos, más eventos de entrada adicionales pueden ingresar, lo que extiende la interacción lenta por mucho más tiempo.
Dado que se trata de una interacción de búsqueda o autocompletado, la eliminación de rebotes de la entrada sería una buena estrategia para que, como máximo, se procese una pulsación de teclas por fotograma.
7. Retraso de entrada
El motivo típico de los retrasos de entrada (el tiempo transcurrido 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 tener varias causas:
- La página se está cargando y el subproceso principal está ocupado realizando el trabajo inicial de configuración del DOM, el diseño y el estilo de la página, y la evaluación y ejecución de 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 en la que, si haces clic en el logotipo del 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 la INP.
Por ejemplo, incluso si no activas otros objetos 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 tiempo notable.
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 de INP.
Este es 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, lo que muestra una duración de procesamiento de 4.9 milisegundos, y la mayor parte de la interacción deficiente se invirtió en el retraso 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, 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 la que retrasó el inicio del procesamiento de la interacción, pero ¿cómo sabríamos por dónde empezar a buscar?
Las entradas de secuencia de comandos 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 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 responsable y dónde se ubicó en nuestros archivos fuente.
8. Retraso de presentación: cuando una actualización no se pinta
El retraso de 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 al usuario comentarios visibles.
Actualiza la página para restablecer el valor de INP de nuevo y, luego, abre el menú de hamburguesas. Hay un problema 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 de presentación el que constituye la mayor parte de la interacción lenta. Eso significa que lo que bloquea 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 entrada única en el array scripts, vemos que el tiempo se invierte en una user-callback de un FrameRequestCallback. Esta vez, el retraso de 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 de INP de una sola carga de página. ¿Cómo se pueden agregar estos datos para depurar la INP en función de los datos de campo? La cantidad de detalles útiles en realidad dificulta más esto.
Por ejemplo, es extremadamente ú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 clase CSS 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 más útil y cómo se pueden agregar los datos. Por ejemplo, antes de enviar datos de atribución, puedes reemplazar el selector web-vitals por un identificador propio, según el componente en el que se encuentra el destino o los roles ARIA que cumple el destino.
Del mismo modo, las entradas scripts pueden tener hashes basados en archivos en sus rutas sourceURL que dificultan su combinación, pero puedes quitar los hashes según tu proceso de compilación conocido antes de enviar los datos.
Lamentablemente, no hay una ruta fácil 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 de INP basada en LoAF es una poderosa ayuda para la depuración. Ofrece datos detallados sobre lo que sucedió específicamente durante una 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 está todo listo para usar 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é 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);