Skip to content

Fundamentos de Long Animation Frames API

Published:

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:

Long Tasks API te dice:

DevTools te da todo el detalle… en local. Pero:

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ónEvent TimingLong TasksDevToolsLoAF
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:

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:

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

  1. Fundamentos de LoAF ← Estás aquí
  2. Debugging de INP (próximamente)
  3. Más Allá de INP (próximamente)
  4. LoAF vs Long Tasks (próximamente)
  5. Third-Party Scripts (próximamente)

Recursos adicionales


Next Post
Glosario de Términos: Long Animation Frames API