Combinando Event Timing API con LoAF
Hemos visto que Event Timing API nos dice cuándo una interacción fue lenta, y LoAF API nos dice qué scripts se ejecutaron en un frame lento.
Pero, ¿cómo correlacionamos ambas APIs para saber exactamente qué script causó que una interacción específica (un click, por ejemplo) fuera lenta?
Este ejemplo muestra cómo combinar ambas APIs para obtener el diagnóstico completo.
Demo interactiva
Análisis de la interacción:
Cómo funciona la correlación
1. Detectar interacción lenta (Event Timing API)
const eventObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 200) {
// Interacción lenta detectada
findRelatedLoaf(entry);
}
}
});
eventObserver.observe({ type: 'event', buffered: true });
2. Buscar el LoAF correspondiente
function findRelatedLoaf(eventEntry) {
// Obtener todos los LoAFs
const loafs = performance.getEntriesByType('long-animation-frame');
// Buscar el que se superpone con el evento
const relevantLoaf = loafs.find(loaf => {
const loafEnd = loaf.startTime + loaf.duration;
const eventEnd = eventEntry.startTime + eventEntry.duration;
// ¿El LoAF y el evento se superponen en tiempo?
return loaf.startTime <= eventEnd && loafEnd >= eventEntry.startTime;
});
return relevantLoaf;
}
3. Identificar el script culpable
if (relevantLoaf) {
console.log('Interacción lenta causada por:');
relevantLoaf.scripts.forEach(script => {
console.log(' - Función:', script.sourceFunctionName);
console.log(' Archivo:', script.sourceURL);
console.log(' Duración:', script.duration + 'ms');
});
}
Ahora sabemos exactamente qué función, en qué archivo, causó que este click específico fuera lento. No hay ambigüedad.
Ventajas de la correlación
| Enfoque | Información |
|---|---|
| Solo Event Timing | "El click tardó 250ms" ❌ ¿Por qué? |
| Solo LoAF | "handleClick() tardó 200ms" ❌ ¿Afectó al usuario? |
| Event Timing + LoAF | "El click tardó 250ms porque handleClick() tardó 200ms" ✅ Completo |
Casos de uso prácticos
Debugging de INP en producción
// Capturar interacciones lentas con contexto completo
const eventObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 200) {
const loaf = findRelatedLoaf(entry);
// Enviar a analytics con contexto completo
sendToAnalytics({
metric: 'slow_interaction',
interactionType: entry.name,
duration: entry.duration,
targetElement: entry.target?.tagName,
scripts: loaf?.scripts.map(s => ({
function: s.sourceFunctionName,
url: s.sourceURL,
duration: s.duration
}))
});
}
}
});
Alertas automáticas
if (entry.duration > 500 && relevantLoaf) {
// Interacción crítica: alertar al equipo
sendSlackAlert({
message: `INP crítico detectado: ${entry.duration}ms`,
element: entry.target?.id,
culprit: relevantLoaf.scripts[0]?.sourceFunctionName,
url: window.location.href,
userAgent: navigator.userAgent
});
}
La correlación temporal no siempre es 1:1. Un evento puede disparar múltiples LoAFs, o un LoAF puede contener scripts de múltiples eventos. Siempre verifica la lógica de overlapping.
Performance de la correlación
Buscar en el array de LoAFs es eficiente porque:
- El buffer de LoAF es limitado (últimos ~200 frames)
- La búsqueda es O(n) donde n es pequeño
- Se ejecuta de forma asíncrona (no bloquea)
Para optimizar aún más:
// Cachear los últimos LoAFs en memoria
const recentLoafs = [];
const MAX_LOAF_CACHE = 50;
const loafObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
recentLoafs.push(entry);
if (recentLoafs.length > MAX_LOAF_CACHE) {
recentLoafs.shift(); // Eliminar el más antiguo
}
}
});
// Buscar solo en el cache
function findRelatedLoaf(eventEntry) {
return recentLoafs.find(loaf => overlaps(loaf, eventEntry));
}