wandres.dev
PERFORMANCE II · Las métricas en vivo

El desglose de INP en sus tres fases

Separar una interacción lenta en retraso de entrada, tiempo de procesamiento y retraso de presentación, y por qué la fase que domina casi nunca es la que la gente supone.

⏱ 19 min

Cuando alguien dice que un botón responde mal, la reacción automática es abrir el manejador del clic y buscar qué hace de más. En un número sorprendente de casos el manejador es trivial y el problema está antes o después de él: en el tiempo que el navegador tardó en poder atenderlo, o en el que tardó en pintar el resultado. Son tres fases con tres arreglos que no se parecen en nada, y el panel las separa.

🎯 Al terminar esta lección sabrás
  • Descomponer una interacción en sus tres fases y leer el desglose en el panel.
  • Diagnosticar qué fase domina y aplicar el arreglo correspondiente.
  • Explicar por qué el retraso de entrada suele deberse a código que no tiene relación con el botón.
  • Medir el desglose en campo con un observador de eventos.

Las tres fases

flowchart LR
a[El usuario pulsa] -->|Retraso de entrada| b[Empieza el manejador]
b -->|Tiempo de procesamiento| c[Terminan los manejadores]
c -->|Retraso de presentacion| d[El fotograma con el cambio se presenta]
style a fill:#cba6f7,color:#11111b
style b fill:#f38ba8,color:#11111b
style c fill:#f9e2af,color:#11111b
style d fill:#a6e3a1,color:#11111b

Retraso de entrada. Desde que el usuario actúa hasta que empieza a ejecutarse el primer manejador. El navegador registró el evento y no pudo atenderlo porque el hilo principal estaba ocupado con otra tarea que no puede interrumpirse. Esta fase no tiene nada que ver con tu botón: es culpa de lo que estuviera corriendo en ese momento.

Tiempo de procesamiento. La ejecución de todos los manejadores registrados para ese evento. Aquí sí está tu código, y también el de cualquier librería o script de terceros que haya puesto un escucha en el mismo evento o en un ancestro.

Retraso de presentación. Desde que terminan los manejadores hasta que el fotograma con el resultado aparece en pantalla. Contiene el recálculo de estilo, la disposición, el pintado y la composición del cambio que acabas de provocar, más cualquier otro trabajo que se colara antes del siguiente fotograma.

En el panel, cada interacción aparece como una caja en su pista, con las tres fases distinguidas al seleccionarla y con la lista de manejadores que se ejecutaron.

Qué significa que cada fase domine

Domina el retraso de entrada. El culpable está en otro sitio del perfil: una tarea larga solapando con el momento del clic. Mira qué es y cuándo empezó. Las causas típicas son un script de terceros inicializándose, un temporizador periódico que hace demasiado, trabajo de hidratación todavía en curso, o un procesamiento de datos que se lanzó al recibir una respuesta. El arreglo nunca está en el manejador: está en partir o mover esa otra tarea.

Hay un caso particular que merece mención porque es contraintuitivo: si el usuario interactúa durante la carga, el retraso de entrada suele ser enorme, y el motivo es que el arranque de la aplicación monopoliza el hilo. Es la razón de que la métrica de respuesta de muchas aplicaciones esté dominada por las interacciones de los tres primeros segundos.

Domina el procesamiento. Aquí sí toca leer el manejador, con dos comprobaciones antes de reescribirlo. La primera: ¿cuántos manejadores hay realmente para ese evento? La lista del panel puede tener sorpresas, incluidos escuchas delegados en ancestros y escuchas de terceros. La segunda: ¿el manejador hace trabajo síncrono que podría ser asíncrono? La respuesta visual al clic —el estado activo, el cargador— necesita ocurrir ya; el resto puede esperar al siguiente fotograma sin que nadie lo note.

Domina la presentación. El manejador terminó rápido y el navegador tardó en pintar. Las causas son un cambio que provoca recálculo de estilo masivo, un cambio que fuerza disposición de todo el documento, o simplemente demasiado DOM nuevo. También, con frecuencia, otro trabajo que se coló entre el manejador y el fotograma: una promesa pendiente que se resolvió justo entonces.

⚠️
Cuidado

Una interacción medida no es un clic: es una unión de eventos. Una pulsación de tecla agrupa la bajada, la subida y el evento de entrada; un toque agrupa el inicio, el final y el clic sintético. La duración que se reporta abarca desde el primero hasta la presentación del último, así que un manejador rápido de teclado puede formar parte de una interacción larga si el evento de entrada asociado hace mucho trabajo.

Medir el desglose en campo

Este observador registra cada interacción con sus tres fases y permite ordenarlas. Funciona en producción y es la instrumentación que convierte una métrica agregada en una lista de culpables.

// Desglose por fases de cada interaccion, agrupado por interactionId
(() => {
  const porInteraccion = new Map();

  new PerformanceObserver(lista => {
    for (const e of lista.getEntries()) {
      if (!e.interactionId) continue;
      const g = porInteraccion.get(e.interactionId) || { eventos: [], inicio: Infinity, fin: 0 };
      g.eventos.push(e);
      g.inicio = Math.min(g.inicio, e.startTime);
      g.fin = Math.max(g.fin, e.startTime + e.duration);
      porInteraccion.set(e.interactionId, g);
    }
  }).observe({ type: 'event', buffered: true, durationThreshold: 16 });

  window.desgloseInteracciones = () => {
    const filas = [];
    for (const [id, g] of porInteraccion) {
      // El evento mas largo del grupo aporta el desglose de fases
      const e = g.eventos.slice().sort((a, b) => b.duration - a.duration)[0];
      const entrada = e.processingStart - e.startTime;
      const proceso = e.processingEnd - e.processingStart;
      const presentacion = (e.startTime + e.duration) - e.processingEnd;
      filas.push({
        id,
        tipo: e.name,
        totalMs: Math.round(g.fin - g.inicio),
        entradaMs: Math.round(entrada),
        procesoMs: Math.round(proceso),
        presentacionMs: Math.round(presentacion),
        dominante: [['entrada', entrada], ['proceso', proceso], ['presentacion', presentacion]]
          .sort((a, b) => b[1] - a[1])[0][0],
        destino: e.target?.tagName?.toLowerCase() +
          (e.target?.id ? '#' + e.target.id : '') || '(desconocido)'
      });
    }
    filas.sort((a, b) => b.totalMs - a.totalMs);
    console.table(filas.slice(0, 20));

    const cuenta = {};
    for (const f of filas) cuenta[f.dominante] = (cuenta[f.dominante] || 0) + 1;
    console.log('Fase dominante por numero de interacciones:', cuenta);
    return filas;
  };

  console.log('Midiendo interacciones. Ejecuta desgloseInteracciones() cuando hayas usado la pagina.');
})();

El resumen final es la parte que orienta el trabajo: si la mayoría de las interacciones lentas están dominadas por el retraso de entrada, no hay ningún manejador que reescribir y todo el esfuerzo tiene que ir a reducir tareas largas. Si están dominadas por la presentación, el trabajo está en el CSS y en la estructura del DOM. Es una decisión de dirección que cuesta un minuto tomar con datos y semanas tomar mal.

El patrón que arregla la mayoría de los casos

Hay una reestructuración de manejador que resuelve una fracción grande de los problemas de respuesta y que consiste en separar el manejador en dos mitades: lo que el usuario tiene que ver ya, y todo lo demás.

// Patron de respuesta inmediata: pintar primero, trabajar despues
async function alPulsar(evento) {
  const boton = evento.currentTarget;

  // 1. Feedback inmediato. Barato y sincrono: entra en este mismo fotograma.
  boton.setAttribute('aria-busy', 'true');
  boton.disabled = true;

  // 2. Ceder el control para que el navegador pinte ese cambio ahora.
  await new Promise(r => requestAnimationFrame(() => setTimeout(r, 0)));

  // 3. Trabajo pesado, ya con la interfaz respondiendo.
  const datos = await calcularAlgoCaro();
  pintarResultado(datos);

  boton.removeAttribute('aria-busy');
  boton.disabled = false;
}

function calcularAlgoCaro() {
  return new Promise(r => {
    const t = performance.now();
    while (performance.now() - t < 120) {}
    r({ resultado: 'listo' });
  });
}
function pintarResultado(d) { console.log('Pintado:', d); }

document.body.addEventListener('click', e => {
  if (e.target.matches('button[data-demo]')) alPulsar(e);
});
document.body.insertAdjacentHTML('beforeend', '<button data-demo>Probar</button>');

La espera combinada del paso dos —un fotograma más una cesión— es la forma fiable de garantizar que el cambio visual se ha pintado antes de empezar el trabajo pesado. Con solo la sincronización de fotograma no basta: esa devolución de llamada se ejecuta antes del pintado, no después, y un error muy común es poner ahí el trabajo caro y bloquear precisamente el fotograma que se quería pintar.

La métrica de respuesta es la única que mide tu aplicación en lugar de tu página, y por eso es la difícil

Merece la pena entender por qué esta métrica resultó ser tanto más difícil de mejorar que las de carga, porque explica dónde está el trabajo de verdad. Las métricas de carga miden un evento único, controlado, que ocurre una vez y en unas condiciones que el equipo puede reproducir: se optimiza el camino crítico de una página y se acabó. La métrica de respuesta mide la peor de todas las interacciones de una sesión entera, que puede durar horas, en cualquier pantalla, con cualquier volumen de datos, en cualquier estado de la aplicación. No hay un camino crítico que optimizar: hay una cola larga de casos malos, y la métrica se queda con el peor. Eso tiene tres implicaciones prácticas. La primera: no se puede mejorar con una optimización, solo con una política. No existe el cambio único que arregle todas las interacciones; existen reglas que impiden que aparezcan interacciones malas, como no hacer trabajo síncrono no acotado en un manejador, o ceder el control antes de cualquier cálculo que dependa del tamaño de una colección. La segunda: el laboratorio la mide mal por construcción. Una sesión de prueba de treinta segundos con cinco clics en la pantalla principal no encuentra la interacción que va mal, que es la de la pantalla que solo usan los usuarios avanzados con mil elementos en su lista. Por eso el campo no es una comprobación posterior sino la única fuente que sabe qué interacción hay que investigar; el laboratorio sirve para diagnosticar la que el campo señaló. La tercera, y la más útil como criterio de diseño: la fase que domina en la mayoría de las aplicaciones reales es el retraso de entrada, no el procesamiento. Es decir, el problema no suele ser que tus manejadores sean lentos sino que el hilo principal está lleno de otras cosas cuando el usuario actúa. Y eso significa que la palanca más potente para mejorar la capacidad de respuesta no está en los manejadores: está en todo el trabajo de fondo que hace la aplicación mientras nadie mira, y que nadie considera un problema de rendimiento porque no bloquea nada visible. Analítica, sincronizaciones, precálculos, sondeos, observadores mal filtrados. Cada uno de ellos es una tarea que puede caer justo cuando el usuario pulsa.