Marzo de 2024. Google hace oficial INP (Interaction to Next Paint) como Core Web Vital, reemplazando a FID. Y con ello, miles de equipos descubren que sus sitios tienen un problema: INP alto. Muy alto.
El número de una métrica no es más que un número, hasta que sabes cómo interpretarlo. Y aquí empieza el verdadero problema.
El problema no es que INP esté alto
El problema es que no sabes por qué.
Imagina este escenario (probablemente te suene):
PageSpeed Insights reporta INP de 350ms en usuarios reales. Mal, pero no terrible. Abres las DevTools, lanzas el click problemático, y ves en Event Timing API: 264ms.
Vale, ya sabemos que es lento. ¿Y ahora qué?
Event Timing te dice:
- ✅ Qué interacción fue lenta (click en botón X)
 - ✅ Cuánto tardó (264ms)
 - ❌ Por qué fue lenta
 
Long Tasks API te dice:
- ✅ Hay una tarea >50ms
 - ❌ Qué script la causó
 - ❌ Qué función específica
 
DevTools te da todo el detalle… en local. Pero:
- ❌ No funciona en producción
 - ❌ No refleja usuarios reales
 - ❌ Requiere horas de análisis manual
 
Resultado: Tu equipo pasa días comentando código, haciendo git bisect, intentando reproducir el problema en local.
He estado ahí. No es divertido.
Long Animation Frames API es la pieza que faltaba
La versión de Chrome 116 (septiembre 2023) añade Long Animation Frames API (LoAF), diseñada específicamente para responder:
¿QUÉ código causó este frame lento?
Ahora no tenemos un mansaje de “hay algo lento”, sinó, qué función, en qué archivo, y cuánto tardó.
Qué te da LoAF que otras herramientas no
| Información | Event Timing | Long Tasks | DevTools | LoAF | 
|---|---|---|---|---|
| Nombre de función | ❌ | ❌ | ✅ | ✅ | 
| Archivo/URL del script | ❌ | ⚠️ Limitado | ✅ | ✅ | 
| Tiempo de forced layout | ❌ | ❌ | ⚠️ Manual | ✅ | 
| Disponible en producción | ✅ | ✅ | ❌ | ✅ | 
| Attribution automática | ❌ | ❌ | ⚠️ Manual | ✅ | 
En otras palabras: LoAF combina el detalle de DevTools con la disponibilidad en producción de Event Timing.
Conceptos que necesitas entender
Antes de meternos con código, dos conceptos críticos.
Layout Thrashing
Layout thrashing es ese anti-patrón que todos hemos escrito alguna vez (yo el primero).
for (let i = 0; i < 100; i++) {
  const height = element.offsetHeight; // ← LEER
  element.style.width = `${height + i}px`; // ← ESCRIBIR
}
¿Ves el problema?
Cada vez que lees offsetHeight, obligas al navegador a calcular el layout. Y eso lo hace en cada iteración, de forma síncrona.
Después escribes style.width, invalidando ese layout que acabas de forzar.
100 iteraciones = 100 recálculos completos. Muy costoso.
La versión eficiente:
// Primero LEER todo
const measurements = elements.map(el => el.offsetHeight);
// Luego ESCRIBIR todo
elements.forEach((el, i) => (el.style.width = `${measurements[i]}px`));
Batch reads, batch writes. Siempre.
Frame Duration vs Script Duration
Esta es la distinción más importante en LoAF.
Frame Duration (entry.duration): Tiempo total del frame completo. Todo. JavaScript, style, layout, paint, composite.
Script Duration (script.duration): Solo el tiempo de ese script específico ejecutando JavaScript.
Frame completo (54.80ms)
│
├─ Script cloudinary (32.00ms)   ← JavaScript
├─ Style calculation (5ms)       ← CSS
├─ Layout (8ms)                  ← Geometría
├─ Paint (7ms)                   ← Píxeles
└─ Composite (2.80ms)            ← GPU
Un script de 32ms puede causar un frame de 55ms. Y LoAF te dice exactamente cuánto de cada cosa.
Tu primer LoAF entry
Vale, menos teoría. Vamos al código.
const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    console.log("Frame lento:", entry.duration.toFixed(2) + "ms");
    entry.scripts.forEach(script => {
      console.log("  →", script.sourceFunctionName);
      console.log("    Invoker:", script.invoker);
      console.log("    Archivo:", script.sourceURL);
      console.log("    Duración:", script.duration.toFixed(2) + "ms");
      console.log(
        "    Forced layout:",
        script.forcedStyleAndLayoutDuration.toFixed(2) + "ms"
      );
    });
  }
});
observer.observe({
  type: "long-animation-frame",
  buffered: true, // Incluye frames anteriores
});
Ejecutemos este script en un entorno de producción para analizar el resultado.
¿Qué obtienes?
{
  duration: 234.5,                // Frame completo
  blockingDuration: 184.5,        // Tiempo bloqueando (duration - 50ms)
  scripts: [{
    invoker: 'BUTTON#checkout.onclick',
    sourceURL: 'https://example.com/app.js',
    sourceFunctionName: 'handleCheckout',
    duration: 230.5,
    forcedStyleAndLayoutDuration: 0  // ← La clave para detectar thrashing
  }]
}
Attribution completa. En producción. Sin DevTools.
¿Por qué 50ms?
Los frames >50ms se consideran “long animation frames”. ¿Por qué ese número?
No es arbitrario:
- 60 FPS = ~16.67ms por frame ideal
 - El RAIL Model recomienda responder a input en <100ms
 - 50ms es el umbral de Long Tasks API
 
La lógica: si una tarea consume >50ms, puede que no queden 50ms para responder en <100ms total.
Es un umbral de detección, no de percepción humana. Una guía, no una regla absoluta.
Categorizar frames por severidad
No todos los frames lentos son igual de urgentes.
function getFrameSeverity(blockingDuration) {
  if (blockingDuration > 200) return { level: "critical", icon: "🔴" };
  if (blockingDuration > 100) return { level: "high", icon: "🟡" };
  if (blockingDuration > 50) return { level: "medium", icon: "🔵" };
  return { level: "low", icon: "🟢" };
}
Importante: Estos umbrales son recomendaciones prácticas, no métricas oficiales de Google.
No confundir con INP:
- INP (oficial): ≤200ms = good, 200-500ms = needs improvement, >500ms = poor
 - Frame duration: Sin umbrales oficiales
 
Un frame de >200ms puede contribuir a un INP alto, pero INP mide el P75 de todas las interacciones, no frames individuales.
¿Por qué blockingDuration y no duration?
entry.duration = 234ms          // Total
entry.blockingDuration = 184ms  // duration - 50ms
Los primeros 50ms son “aceptables” (el umbral de LoAF). blockingDuration mide solo el tiempo excedente que bloquea la UI.
Refleja mejor el impacto real en usuarios.
Los umbrales 50/100/200ms explicados
50ms: Umbral mínimo de LoAF. ~3 frames perdidos a 60fps. Basado en RAIL y Long Tasks API.
100ms: Límite para respuesta percibida como instantánea (RAIL Model). Usuario empieza a notar la lentitud.
200ms: Frames >200ms pueden contribuir significativamente a INP poor (>500ms). Experiencia muy degradada.
Lo que viene
Has visto lo básico: qué es LoAF, cómo capturar frames, cómo categorizarlos.
En el siguiente artículo veremos cómo correlacionar LoAF con INP para identificar exactamente qué script causó una interacción lenta específica.
Spoiler: es más fácil de lo que piensas.
Serie completa
- Fundamentos de LoAF ← Estás aquí
 - Debugging de INP (próximamente)
 - Más Allá de INP (próximamente)
 - LoAF vs Long Tasks (próximamente)
 - Third-Party Scripts (próximamente)
 
Recursos adicionales
- Glosario - Referencia completa de términos y conceptos