1. Introducción
Este es un codelab interactivo para aprender a medir la interacción con la siguiente pintura (INP) con la biblioteca web-vitals
.
Requisitos previos
- Conocimientos de desarrollo de HTML y JavaScript
- Opción recomendada: Lee la documentación de métricas de INP de web.dev.
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 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 de web-vitals-codelabs
.
- Clona el repositorio 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 de referencia popular sobre la anatomía de los caracoles) para explorar posibles problemas con la INP.
Intenta interactuar con la página para saber qué interacciones son lentas.
3. Cómo familiarizarte con las Herramientas para desarrolladores de Chrome
Abre DevTools desde el menú Más herramientas > Herramientas para desarrolladores, haciendo clic con el botón derecho en la página y seleccionando Inspeccionar o usando 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 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. Cómo instalar web-vitals
web-vitals
es una biblioteca de JavaScript para medir las métricas de las 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 analizarlos más adelante. Para nuestros fines, queremos saber cuándo y dónde ocurren las interacciones lentas.
Existen varias formas de agregar la biblioteca a una página. La forma en que instales 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 ver todas las opciones.
En este codelab, se instalará desde npm y se cargará la secuencia de comandos directamente para evitar entrar 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 las Métricas web esenciales en una carga de página.
- La compilación "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 la 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 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 sale de la página o la cierra. Este es el comportamiento ideal para los píxeles contadores para algo como las estadísticas, pero no es 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 se informa cada vez que hay una interacción más lenta que la 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. Ahora, las interacciones deberían informarse a la consola y actualizarse cada vez que haya una nueva interacción más lenta. Por ejemplo, intenta escribir en el cuadro de búsqueda y, luego, borra la entrada.
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 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 Yes para aceptar las cookies (de demostración) y observa los datos de INP que ahora se registran en la consola de DevTools.
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 desde que el usuario hizo clic hasta la siguiente pintura fue de 344 milisegundos, una INP de"necesita mejoras". El array entries
tiene todos los valores PerformanceEntry
asociados con esta interacción; en este caso, solo un evento de clic.
Sin embargo, para saber qué sucede durante este tiempo, nos interesa más la propiedad attribution
. Para compilar los datos de atribución, web-vitals
encuentra qué marco de animación larga (LoAF) se superpone con el evento de clic. Luego, el LoAF puede proporcionar datos detallados sobre cómo se pasó el tiempo durante ese marco, desde las secuencias de comandos que se ejecutaron hasta el tiempo que se pasó en una devolución de llamada, un estilo y un diseño de requestAnimationFrame
.
Expande la propiedad attribution
para ver más información. Los datos son mucho más ricos.
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, el tiempo se desglosa de forma general:
inputDelay
: Es el tiempo que transcurre entre el momento en que el usuario inicia la interacción (por ejemplo, hace clic con el mouse) y el momento en que comienza a ejecutarse el objeto de escucha de eventos para esa interacción. En este caso, la demora de entrada fue de solo unos 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 tienen varios objetos de escucha para un solo evento (por ejemplo,pointerdown
,pointerup
yclick
). Si todos se ejecutan en el mismo fotograma de animación, se combinarán en este momento. En este caso, la duración del procesamiento es de 295.6 milisegundos, la mayor parte del tiempo de INP.presentationDelay
: Es el tiempo 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 para optimizar INP tiene más información sobre este tema.
Si profundizamos un poco más, vemos que processedEventEntries
contiene cinco eventos, a diferencia del único evento 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 INP, en este caso, un clic. La atribución processedEventEntries
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 vital si también fueron lentos, ya que todos contribuyeron a la lentitud de la capacidad de respuesta.
Por último, está el array longAnimationFrameEntries
. Puede ser una sola entrada, pero hay casos en los que una interacción puede extenderse a 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 la cantidad de tiempo dedicado a aplicar diseño. En el artículo de la API de Long Animation Frames, se profundiza en estas propiedades. En este 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 una sola event-listener
, invocada 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.
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.interactionTarget
y la propiedadinvoker
en una entrada de atribución de secuencia de comandos). - El tiempo se dedicó principalmente a ejecutar objetos de escucha de eventos (de
attribution.processingDuration
en comparación con la métrica totalvalue
). - El código del objeto de escucha de eventos lentos comienza desde un objeto de escucha de clics definido en
third-party/cmp.js
(desdescripts.sourceURL
).
Esos son suficientes datos para saber dónde debemos realizar optimizaciones.
6. Múltiples objetos de escucha de eventos
Actualiza la página para que la consola de DevTools esté clara 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á pasando?
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 la 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.
Paginación excesiva de diseños
La primera entrada del array scripts
nos brinda información contextual valiosa:
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 esta ejecución de secuencia de comandos, que es un objeto de escucha input
(el llamador 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 es el tiempo que se dedicó 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ó a la fragmentación del diseño.
Esta debe ser una prioridad principal para solucionarlo.
Objetos de escucha repetidos
De forma individual, 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 keyup
que se ejecutan una 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 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 de la misma 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 nada.
Este efecto también puede aumentar, ya que cuanto más tiempo tarden en completarse los objetos de escucha de eventos, más eventos de entrada adicionales pueden ingresar, lo que extiende la interacción lenta mucho más.
Como se trata de una interacción de búsqueda o autocompletar, una buena estrategia sería anular 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 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 puede 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 estilo a la página, y evaluar y ejecutar secuencias de comandos.
- Por lo general, la página está 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 que, 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 de la caracola 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 alta puedes activar la INP.
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 el caracol rebota), 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 de INP.
Este es un ejemplo de atribución de solo enfocar el cuadro de búsqueda durante el rebote de 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 gran mayoría de la interacción deficiente se destinó a la demora de entrada, lo que tomó 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 algo más en la página lo que retrasó el inicio del procesamiento de la interacción, pero ¿cómo sabríamos 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, ralentizó el fotograma de animación y, por lo tanto, 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 (a través de un objeto de escucha animationiteration
), qué función fue responsable exactamente y dónde se encontraba en nuestros archivos fuente.
8. Retraso de presentación: Cuando una actualización no se pinta
La demora de presentación mide el tiempo desde que los objetos de escucha de eventos terminan de ejecutarse hasta que el navegador puede pintar un nuevo fotograma en la pantalla y mostrarle al usuario los comentarios visibles.
Actualiza la página para restablecer el valor de INP nuevamente y, luego, abre el menú de opciones. 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, la demora de presentación es la que representa 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 única entrada del array scripts
, vemos que el tiempo se dedica a un user-callback
desde un FrameRequestCallback
. Esta vez, la demora de la presentación se debe a una devolución de llamada requestAnimationFrame
.
9. Conclusión
Cómo agregar 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 desde 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 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 tiene nombres de clases CSS compilados que cambian de compilación a compilación, los selectores web-vitals
del mismo elemento pueden ser diferentes en cada compilación.
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 volver a enviar los datos de atribución de píxeles contadores, puedes reemplazar el selector web-vitals
por un identificador propio, según el componente en el que se encuentra el objetivo o los roles de ARIA que cumple el objetivo.
De manera similar, las entradas scripts
pueden tener valores hash basados en archivos en sus rutas de acceso sourceURL
que dificultan su combinación, pero puedes quitar los valores hash en función de tu proceso de compilación conocido antes de volver a 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 en absoluto para el proceso de depuración.
Atribución en todas partes
La atribución de INP basada en LoAF es una herramienta de depuración potente. 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 los datos de atribución de INP en cualquier sitio.
Incluso si no tienes acceso para editar una página, puedes volver a crear el proceso de este codelab. Para ello, ejecuta el siguiente fragmento de código en la consola de DevTools y observa 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);