Medir · 3 min de lectura
Desmontar el INP: dónde se va el tiempo entre el clic y la imagen
Interaction to Next Paint mide lo rápido que una página responde de forma visible. La cifra tiene tres partes y cada una tiene otra causa.
Desde 2024, Interaction to Next Paint (INP) es una de las Core Web Vitals y sustituyó a First Input Delay. INP observa todos los clics, toques y pulsaciones de teclas durante una visita e informa aproximadamente de la más lenta, medida desde el momento de la entrada hasta la imagen en la que se ve la respuesta. El desplazamiento y el hover no cuentan.
Se considera bueno hasta 200 ms, necesita mejorar hasta 500 ms y malo por encima, siempre referido al percentil 75 de todas las visitas.
Tres partes
- Retraso de entrada: el tiempo hasta que puede empezar el primer handler. Aparece cuando el hilo principal está ocupado con otra cosa, típicamente una tarea larga.
- Procesamiento: lo que tardan todos los handlers de ese evento, es decir, tu propio código.
- Retraso de presentación: desde el final de los handlers hasta la siguiente imagen: callbacks de
requestAnimationFrame, Style, Layout, Paint. Los cambios grandes en el DOM encarecen esta parte.
Medir
La Event Timing API entrega las tres partes de cada interacción. Las entradas con el mismo interactionId pertenecen a la misma interacción (por ejemplo pointerdown, pointerup y click):
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
if (!e.interactionId) continue; // no es un clic, toque ni tecla
const inputDelay = e.processingStart - e.startTime;
const processing = e.processingEnd - e.processingStart;
const presentation = e.startTime + e.duration - e.processingEnd;
console.log(e.name, e.duration, { inputDelay, processing, presentation });
}
}).observe({ type: 'event', durationThreshold: 16, buffered: true });
durationThreshold fija a partir de qué duración se informan las entradas; 16 ms es el mínimo y, si no se indica, son 104 ms. La duración en sí se redondea a 8 ms, así que no conviene interpretar decimales.
Reducir cada parte por separado
- Retraso de entrada: dividir las tareas largas (ver Encontrar tareas largas antes que tus usuarios), sobre todo durante la carga, cuando se evalúan scripts y se inicializan componentes.
- Procesamiento: en el handler, hacer solo lo necesario para la respuesta visible y aplazar el resto.
- Presentación: cambiar menos DOM de una vez, evitar el layout thrashing (ver Primero leer, luego escribir: evitar el layout thrashing) y sacar del renderizado las zonas grandes que no se ven con
content-visibility: auto.
Para el punto del medio funciona bien un patrón sencillo: primero la respuesta, después el trabajo:
button.addEventListener('click', async () => {
button.classList.add('busy'); // respuesta visible enseguida
await yieldToMain(); // ahora el navegador puede dibujar
const result = compute(); // la parte cara, después
render(result);
});
yieldToMain() es la misma pequeña función de la nota sobre tareas largas. Para el INP, la interacción termina con la primera imagen en la que el botón aparece ocupado. El cálculo posterior ya no cuenta para esa interacción, aunque, claro, retrasará la siguiente si es a su vez una tarea larga.