
En el [artículo anterior](/posts/loaf/fundamentos/) vimos los fundamentos de la API de Long Animation Frames (LoAF) y cómo podemos utilizarla para capturar frames lentos.

Pero capturar los frames lentos es solo la primera parte del trabajo. Ahora nos enfrentamos a la tarea más importante: **entender qué es lo que está causando esos frames lentos y, en última instancia, rompiendo nuestro INP**.

Porque no es lo mismo ver un reporte de <a href="https://developers.google.com/web/tools/lighthouse" target="_blank">Lighthouse</a> o <a href="https://pagespeed.web.dev/" target="_blank">PageSpeed Insights</a> que nos dice que tenemos un INP de 350ms, que saber con precisión que la función `handleCheckout()`, en el fichero `app.js` en la línea 234, se está ejecutando durante 280ms y forzando 15 layouts en un bucle. Esta última información es la que nos permite actuar y solucionar el problema.

En este artículo, vamos a ver cómo podemos pasar de la incertidumbre de "algo va lento" a la certeza de "esto es lo que lo rompe".

## El problema de las herramientas por separado

Para entender el valor de LoAF, primero debemos comprender las limitaciones de las herramientas que teníamos hasta ahora.

Por un lado, la **Event Timing API** nos da información sobre la interacción del usuario:

```javascript
{
  name: 'click',
  duration: 250ms,
  target: button#checkout
}
```

Esto es útil, pero nos deja con una pregunta importante: **sabemos qué interacción fue lenta, pero no por qué**. ¿Qué parte del código causó esa lentitud?

Por otro lado, **Long Animation Frames API (LoAF)** nos da información sobre el código que se ejecutó:

```javascript
{
  duration: 200ms,
  scripts: [{
    sourceFunctionName: 'handleClick',
    duration: 200ms
  }]
}
```

Esto también es útil, pero de nuevo, nos deja con otra pregunta: **sabemos qué código se ejecutó, pero no si afectó a una interacción del usuario**.

El verdadero poder reside en **combinar ambas APIs**. Cuando las usamos juntas, podemos contar la historia completa:

> "La interacción de 'click' en el botón de checkout tardó 250ms **porque** la función `handleClick()` tardó 200ms en ejecutarse."

Ahora sí tenemos una visión completa del problema. No solo sabemos qué fue lento, sino también por qué. Y con esa información, podemos empezar a trabajar en una solución.

## Correlacionar Event Timing con LoAF

Como hemos visto, la clave para obtener una visión completa del rendimiento es correlacionar la información de Event Timing con la de LoAF. La técnica para hacerlo es sorprendentemente sencilla: tenemos que buscar una superposición en el tiempo (overlapping) entre los eventos de interacción y los frames de animación largos.

Vamos a ver cómo podemos implementar esto con código.

Primero, creamos un `PerformanceObserver` para escuchar los eventos de interacción que superen un determinado umbral. En este caso, nos interesan los eventos que duren más de 200ms.

```javascript
const eventObserver = new PerformanceObserver(list => {
  // Solo obtendremos los eventos que superan los 200ms
  // ya que le hemos definido en el observer con durationThreshold
  for (const entry of list.getEntries()) {
    // Buscamos el LoAF que se corresponda con este evento
    const culprit = findRelatedLoaf(entry);

    if (culprit) {
      // Si lo encontramos, ya tenemos al culpable
      console.log("Interacción lenta:", entry.name);
      console.log("Causada por:", culprit.scripts[0].sourceFunctionName);
    }
  }
});

eventObserver.observe({
  type: "event",
  buffered: true,
  durationThreshold: 200, // Solo nos notifica de eventos >200ms
});
```

### La función mágica: `findRelatedLoaf`

La función `findRelatedLoaf` es el corazón de esta técnica. Su objetivo es encontrar el "long animation frame" (LoAF) que se solapa en el tiempo con el evento de interacción que estamos analizando.

```javascript
function findRelatedLoaf(eventEntry) {
  // Obtenemos todas las entradas de LoAF que se han registrado
  const loafs = performance.getEntriesByType("long-animation-frame");

  // Buscamos la primera que se solape en el tiempo con nuestro evento
  return loafs.find(loaf => {
    const loafEnd = loaf.startTime + loaf.duration;
    const eventEnd = eventEntry.startTime + eventEntry.duration;

    // La condición de solapamiento:
    // El LoAF empieza antes de que el evento termine Y
    // el LoAF termina después de que el evento empiece.
    return loaf.startTime <= eventEnd && loafEnd >= eventEntry.startTime;
  });
}
```

Para entender mejor la lógica del solapamiento, podemos visualizarlo de la siguiente manera:

![Event-LoAF](https://res.cloudinary.com/nucliweb/image/upload/v1762359660/joanleon.dev/assets/loaf/debugging-inp/Evento-LoAF.svg)

Si el rango de tiempo de un LoAF se superpone con el de un evento de interacción, es muy probable que el frame largo sea el causante de la lentitud de dicha interacción.

Como vemos, la lógica es sencilla, pero nos aporta una información muy valiosa para entender y solucionar los problemas de rendimiento.

## Optimización: cache de LoAFs

La implementación anterior tiene un problema de eficiencia. La función `findRelatedLoaf` llama a `performance.getEntriesByType("long-animation-frame")` cada vez que se produce un evento de interacción lento. Esto puede ser ineficiente si tenemos muchos eventos, ya que estamos pidiendo al navegador que nos dé la lista completa de LoAFs una y otra vez.

Una solución mucho más eficiente es cachear los LoAFs en memoria a medida que se van produciendo. De esta manera, cuando necesitemos buscar un LoAF, lo haremos sobre una lista mucho más pequeña y que ya tenemos en memoria.

Para implementar esta cache, podemos usar otro `PerformanceObserver` que se encargue de escuchar los LoAFs y los vaya guardando en un array.

```javascript
// Array que usaremos como cache
const recentLoafs = [];

const loafObserver = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    // Añadimos el nuevo LoAF a nuestro cache
    recentLoafs.push(entry);

    // Para evitar que el array crezca indefinidamente,
    // mantenemos solo los últimos 50 LoAFs.
    // Este es un mecanismo FIFO (First-In, First-Out).
    if (recentLoafs.length > 50) {
      recentLoafs.shift();
    }
  }
});

// Empezamos a observar los LoAFs
loafObserver.observe({ type: "long-animation-frame", buffered: true });
```

Con esta implementación, la función `findRelatedLoaf` ya no necesita llamar a `performance.getEntriesByType()`. En su lugar, puede buscar directamente en nuestro array `recentLoafs`, que como máximo tendrá 50 elementos.

Esto hace que el proceso de búsqueda sea mucho más rápido y con menos sobrecarga (overhead) para el navegador. Pasamos de buscar en una lista potencialmente grande del Performance Timeline a buscar en un pequeño array en memoria.

> Esto me recuerda la pregunta que me hizo [Javier Vélez](https://www.linkedin.com/in/javiervelezreyes/) en la charla [Midiendo lo invisible con Long Animation Frames API](https://www.youtube.com/live/rW9eZOfXqYY) sobre el overhead que añaden los scripts de observabilidad. Es importante tener en cuenta que debemos sobrecargar lo mínimo posible el navegador, pero como le respondí: **"No podemos mejorar lo que no medimos"**. Por lo tanto, es un equilibrio con el que tenemos que lidiar.

## Encontrar el culpable específico

Una vez que hemos identificado el "Long Animation Frame" (LoAF) que ha causado la interacción lenta, el siguiente paso es averiguar qué script específico dentro de ese frame es el responsable. Un LoAF puede contener la ejecución de múltiples scripts, y necesitamos saber cuál de ellos es el que está causando el problema.

En la mayoría de los casos, el culpable será el script que más tiempo ha tardado en ejecutarse. Por lo tanto, podemos encontrarlo buscando el script con la duración más alta dentro del LoAF.

Veamos una función que hace exactamente eso:

```javascript
function findCulprit(loaf) {
  // Usamos reduce para encontrar el script con la duración más alta.
  // Partimos de un objeto con duración 0.
  return loaf.scripts.reduce(
    (max, script) => (script.duration > max.duration ? script : max),
    {
      duration: 0,
    }
  );
}

// Así la podemos usar:
const relevantLoaf = findRelatedLoaf(entry); // La función que vimos antes
if (relevantLoaf) {
  const badScript = findCulprit(relevantLoaf);
  console.log(
    `El script culpable es ${badScript.sourceFunctionName} y tardó ${badScript.duration}ms`
  );
}
```

Esta función `findCulprit` itera sobre todos los scripts del LoAF y devuelve el que tiene la mayor duración. Aunque es una heurística simple, en la gran mayoría de los casos (podríamos decir que en más del 90%) nos señalará al causante real del problema de rendimiento.

## Enviar a un sistema de monitorización de usuarios reales RUM (producción)

Hemos visto cómo identificar interacciones lentas y sus culpables en nuestro entorno de desarrollo. Pero el verdadero valor de esta técnica reside en aplicarla en producción, con usuarios reales. Para ello, necesitamos enviar esta información a un sistema de monitorización de usuarios reales (Real User Monitoring o RUM), que puede ser una herramienta de analítica como Google Analytics o una plataforma especializada.

La idea es capturar la misma información que hemos estado viendo, pero en lugar de mostrarla en la consola, la enviamos a nuestro sistema de RUM.

```javascript
const eventObserver = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    // Nos aseguramos de que la interacción es lenta y tiene un ID
    if (entry.duration > 200 && entry.interactionId) {
      const loaf = findRelatedLoaf(entry);
      const culprit = loaf ? findCulprit(loaf) : null;

      // Enviamos los datos a nuestro sistema de analítica
      sendToAnalytics({
        metric: "inp",
        value: entry.duration,
        interaction: entry.name,
        element: entry.target?.tagName,

        // Aquí está la información clave que hemos obtenido
        function: culprit?.sourceFunctionName,
        script_duration: culprit?.duration,
        script_url: culprit?.sourceURL,

        // Y añadimos contexto adicional
        page: location.pathname,
        timestamp: Date.now(),
      });
    }
  }
});
```

La parte más importante de este código es el objeto que enviamos a `sendToAnalytics`. Además de la información básica de la interacción, estamos enviando los detalles del script culpable que hemos obtenido gracias a LoAF. Esta es la "salsa secreta" que nos permitirá tener una visión sin precedentes del rendimiento de nuestra aplicación en producción.

Con esta información en nuestro sistema de RUM, podemos pasar de tener un simple número ("el INP es de 380ms") a tener un informe detallado y accionable:

> "El INP P95 es de 380ms. El principal culpable es la función `handleCheckout`, que está implicada en el 45% de las interacciones lentas, sobre todo en la página `/checkout`."

Esto nos permite dejar de depurar a ciegas y empezar a tomar decisiones informadas sobre qué optimizar y dónde.

## Del debugging al dashboard: análisis agregado

La correlación de eventos individuales es una herramienta muy potente para el debugging en local. Nos permite entender por qué una interacción específica ha sido lenta. Sin embargo, cuando trabajamos en producción, necesitamos una visión más amplia. No nos interesa tanto un caso aislado, sino entender **qué scripts están causando problemas de rendimiento de forma sistemática**.

### El problema del análisis puntual

Si nos quedamos en el análisis de casos individuales, corremos el riesgo de sacar conclusiones equivocadas. Un mensaje como "Este click fue lento por `handleClick()`" es útil, pero no nos da el contexto completo. ¿Fue un caso aislado? ¿O es un problema recurrente? ¿Quizás `handleClick()` solo es lento en determinadas circunstancias?

Para responder a estas preguntas, necesitamos pasar del análisis puntual a un análisis agregado.

### La solución: agregación de datos

La solución es registrar y agregar la información de **todos** los scripts que se ejecutan en los "Long Animation Frames" a lo largo de la sesión de un usuario o usuaria. De esta manera, podemos identificar patrones y encontrar a los verdaderos culpables de los problemas de rendimiento.

Podemos implementar esta agregación de datos utilizando un `Map` para almacenar las estadísticas de cada script:

```javascript
const scriptStats = new Map();

const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    entry.scripts.forEach(script => {
      const key = script.sourceFunctionName || "anonymous";

      if (!scriptStats.has(key)) {
        scriptStats.set(key, {
          functionName: key,
          count: 0,
          totalDuration: 0,
          maxDuration: 0,
          minDuration: Infinity,
        });
      }

      const stats = scriptStats.get(key);
      stats.count++;
      stats.totalDuration += script.duration;
      stats.maxDuration = Math.max(stats.maxDuration, script.duration);
      stats.minDuration = Math.min(stats.minDuration, script.duration);
    });
  }
});

observer.observe({ type: "long-animation-frame", buffered: true });
```

### Identificando a los principales culpables (Top Offenders)

Una vez que tenemos las estadísticas de todos los scripts, podemos crear una función que nos devuelva una lista de los que más impacto tienen en el rendimiento.

```javascript
function getTopOffenders(limit = 10) {
  return Array.from(scriptStats.values())
    .sort((a, b) => b.totalDuration - a.totalDuration)
    .slice(0, limit);
}

// El resultado sería algo así:
[
  {
    functionName: "handleCheckout",
    count: 47,
    totalDuration: 8200, // 8.2 segundos de tiempo de bloqueo acumulado
    maxDuration: 350,
  },
  {
    functionName: "validateForm",
    count: 124,
    totalDuration: 6800,
    maxDuration: 180,
  },
];
```

Con esta información, ahora sí tenemos una visión clara de dónde debemos centrar nuestros esfuerzos de optimización. Ya no estamos a ciegas; tenemos datos que nos guían.

## Estrategias de priorización

Una vez que tenemos los datos agregados, el siguiente paso es decidir qué optimizar primero. No todos los scripts lentos son igual de importantes, y es fundamental priorizar nuestros esfuerzos para obtener el mayor impacto posible.

Aquí te comparto algunas estrategias de priorización que podemos seguir:

### 1. Por impacto total (Total Duration)

Esta es la estrategia más directa. Consiste en ordenar los scripts por su `totalDuration` de forma descendente. El script que más tiempo de bloqueo ha acumulado es, en teoría, el que más está contribuyendo a una mala experiencia de usuario.

Por ejemplo, si `handleCheckout` acumula 8.2 segundos de tiempo de bloqueo, es un candidato claro para ser optimizado. Solucionar este problema probablemente nos dará el mayor retorno de inversión (ROI) en términos de mejora del rendimiento.

### 2. Por frecuencia (Count)

Otra estrategia es ordenar los scripts por el número de veces que se han ejecutado (`count`). Un script que se ejecuta con mucha frecuencia, aunque no sea el más lento en términos de duración individual, puede ser un síntoma de un problema de arquitectura.

Por ejemplo, si `validateForm` se ejecuta 124 veces, deberíamos preguntarnos: ¿es normal que esta función se ejecute tantas veces? ¿Podemos reducir el número de llamadas? A veces, optimizar la frecuencia de ejecución puede tener un impacto mayor que optimizar la duración de una única llamada.

### 3. Por peor caso (Max Duration)

Finalmente, podemos ordenar los scripts por su `maxDuration`. Esta estrategia nos ayuda a identificar los "casos extremos" (edge cases) que pueden estar causando una experiencia de usuario muy pobre para un subconjunto de nuestras visitas.

Por ejemplo, si `loadWidget` tuvo una duración máxima de 850ms, es una señal de alarma. Aunque solo haya ocurrido una vez, una interacción de casi un segundo es inaceptable. En este caso, la pregunta a responder es: ¿qué condiciones específicas hicieron que este script tardara tanto?

### Visualizando el impacto con porcentajes

Para tener una idea más clara del impacto de cada script, podemos calcular el porcentaje de tiempo de bloqueo que representa cada uno sobre el total.

```javascript
const total = Array.from(scriptStats.values()).reduce(
  (sum, s) => sum + s.totalDuration,
  0
);

getTopOffenders().forEach(script => {
  const percentage = (script.totalDuration / total) * 100;
  console.log(
    `${script.functionName}: ${percentage.toFixed(1)}% del tiempo de bloqueo total`
  );
});

// Output:
// handleCheckout: 28.3% del tiempo de bloqueo total
// validateForm: 23.5% del tiempo de bloqueo total
// loadWidget: 15.2% del tiempo de bloqueo total
// ...
```

Si `handleCheckout` es responsable del 28% de todo el tiempo de bloqueo de nuestra aplicación, hemos encontrado a nuestro principal enemigo.

## Lo que aprendiste

A lo largo de este artículo, hemos visto un conjunto de técnicas que nos permiten pasar de la incertidumbre a la acción en lo que respecta a la optimización del INP.

Hemos aprendido a:

1.  **Correlacionar Event Timing con LoAF:** La clave para entender no solo _qué_ interacción fue lenta, sino _por qué_. Hemos visto cómo la superposición temporal nos permite conectar una interacción de usuario con el frame de animación largo que la ralentizó.
2.  **Optimizar la captura de datos con un cache:** Para evitar problemas de rendimiento en nuestra propia monitorización, hemos implementado un cache en memoria que nos permite buscar LoAFs de forma mucho más eficiente.
3.  **Agregar datos para una visión global:** Hemos pasado del análisis de casos aislados a una visión agregada que nos permite identificar los scripts que causan problemas de rendimiento de forma sistemática en nuestra aplicación.
4.  **Priorizar las optimizaciones con estrategias claras:** Hemos visto diferentes formas de analizar los datos agregados para decidir dónde centrar nuestros esfuerzos y obtener el mayor impacto posible en la experiencia de nuestros usuarios.

Ahora, el trabajo de depurar ya no lo hacemos a ciegas. Con LoAF tenemos mucha más información para poder saber qué está afectando al rendimiento y conseguir datos para priorizar las tareas de optimización.

---

En el siguiente artículo veremos cómo LoAF nos ayuda **más allá de INP**: detectar frames que bloquean la métrica LCP y distinguir JavaScript pesado de layout thrashing.

Spoiler: `forcedStyleAndLayoutDuration` es más importante de lo que piensas.

---

### Ejemplos Relacionados

- <a href="https://loaf-ejemplos.vercel.app/examples/05-inp-button.html" target="_blank">Ejemplo 05: Correlacionar LoAF con INP en un botón</a>
- <a href="https://loaf-ejemplos.vercel.app/examples/06-lcp-blocking.html" target="_blank">Ejemplo 06: Dashboard de scripts responsables</a>

## Serie completa

1. [Fundamentos de LoAF](/posts/loaf/fundamentos/)
2. **Debugging de INP** → Estás aquí
3. [Más Allá de INP: LCP y el 90% invisible del rendimiento](/posts/loaf/mas-alla-del-inp/)
4. LoAF vs Long Tasks _(próximamente)_
5. Third-Party Scripts _(próximamente)_

## Recursos adicionales

<!--- 📄 **[Setup Básico](./setup-basico.md)** - Código copy-paste para empezar a usar LoAF-->

- **[Glosario](/posts/loaf/glosario/)** - Referencia completa de términos y conceptos

<!--[← Volver al índice](../README.md)-->
