El build de atribución: de la métrica a la causa
Qué información de diagnóstico añade cada métrica en el build de atribución, cómo se lee, y qué campos hay que enviar para poder arreglar sin volver a mirar.
Saber que tu LCP está en 3,8 segundos no te dice nada accionable. Saber que el elemento es la imagen del hero, que su descubrimiento tardó 1,2 segundos y que el retraso de renderizado fue de 900 milisegundos convierte el número en tres tareas concretas. Esa diferencia cuesta un kilobyte y medio y es la mejor inversión de todo el capítulo de instrumentación.
- Enumerar los campos de atribución de cada métrica y qué diagnóstico habilita cada uno.
- Diseñar el conjunto mínimo de campos a enviar sin desbordar la petición.
- Leer un desglose de subpartes y traducirlo a una acción.
- Personalizar cómo se identifican los elementos en tu propio código.
Qué añade la atribución a cada métrica
LCP: el elemento y las cuatro subpartes
El objeto attribution de LCP trae lo siguiente:
target: un selector que identifica el elemento LCP. Este es el campo que hay que enviar siempre, porque sin él no puedes distinguir una regresión real de un cambio de diseño que ha cambiado cuál es el elemento más grande.url: la URL del recurso de imagen del LCP, si lo hay. Si el elemento es un nodo de texto, no viene.timeToFirstByte,resourceLoadDelay,resourceLoadDurationyelementRenderDelay: las cuatro subpartes en las que se descompone el LCP, sin solapamiento ni huecos, y que suman el total.navigationEntry,lcpResourceEntryylcpEntry: las entradas de rendimiento en crudo, útiles en depuración y demasiado grandes para enviarlas.
Las cuatro subpartes son el marco que convierte una métrica en una tarea. Su lectura es la siguiente:
| Subparte | Qué significa que sea alta | Dirección del arreglo |
|---|---|---|
| TTFB | El servidor o la red tardan antes de que empiece nada | Redirecciones, caché en el borde, tiempo de proceso |
| Retraso de carga del recurso | El navegador tardó en descubrir el recurso | Hacerlo visible en el HTML inicial, subir su prioridad |
| Duración de la carga del recurso | El recurso tarda en transferirse | Menos bytes, formato mejor, distancia menor |
| Retraso de renderizado | El recurso llegó pero no se pudo pintar | Desbloquear el renderizado, reducir tareas largas |
Los dos tramos con la palabra retraso deberían tender a cero: son espera pura. Los otros dos son trabajo real que no se puede eliminar del todo. Hay un reparto de referencia publicado para una página bien optimizada: alrededor del 40% en TTFB, menos del 10% en retraso de carga, alrededor del 40% en duración de la carga y menos del 10% en retraso de renderizado. Son orientaciones, no reglas: si tu LCP está consistentemente por debajo de 2,5 segundos, las proporciones dan igual.
INP: la interacción y sus tres partes
El objeto attribution de INP es el más rico de los tres.
interactionTarget: selector del elemento con el que el usuario interactuó primero en el fotograma de la interacción culpable. Si viene vacío, normalmente significa que el elemento se eliminó del DOM después de la interacción.interactionTime: cuándo ocurrió.interactionType:pointerpara eventos de puntero y clic,keyboardpara teclado.inputDelay,processingDurationypresentationDelay: las tres partes de la latencia.loadState: el estado de carga del documento cuando ocurrió la interacción. Toma los valoresloading,dom-interactive,dom-content-loadedycomplete. Es el campo que distingue un problema de manejadores de un problema de carga disfrazado.longAnimationFrameEntries: si el navegador soporta la API de fotogramas de animación largos, las entradas que se solapan con la interacción.longestScript: resumen del script más largo que se solapa con la duración del INP, con su entrada, la subparte en la que se ejecutó, y cuánto tiempo se solapó. Solo aparecen scripts de más de 5 milisegundos.totalScriptDuration,totalStyleAndLayoutDuration,totalPaintDurationytotalUnattributedDuration: el desglose completo del tiempo, atribuido por categoría.
El campo longestScript merece atención especial porque es lo más cerca que se puede estar de un perfilador en campo. Te dice qué script concreto se comió el tiempo y en qué tramo de la interacción se ejecutó. Con eso y el selector del elemento, muchas veces se identifica el problema sin necesidad de reproducirlo.
El campo totalUnattributedDuration es sutil e informativo: recoge el tiempo que no se pudo atribuir, que incluye scripts de menos de 5 milisegundos y los casos en que el fotograma duró menos de 50 milisegundos y por tanto no generó una entrada de fotograma largo. Cuando no hay entradas de fotograma largo en absoluto, este campo viene indefinido en lugar de contener todo el tiempo, precisamente para que se note la diferencia entre “no hay datos” y “el tiempo está repartido”.
Haz un histograma de loadState sobre tus peores interacciones. Si la masa está en loading o dom-interactive, tu problema de INP es un problema de carga: el usuario interactúa mientras el hilo principal ejecuta el bundle, y ningún ajuste de tus manejadores lo va a arreglar. Si la masa está en complete, el problema sí está en el código que responde a la interacción, y ahí es donde hay que mirar.
CLS: el mayor desplazamiento
El objeto attribution de CLS se centra en el desplazamiento individual más grande que contribuyó a la puntuación:
largestShiftTarget: selector del primer elemento, en orden del documento, que se desplazó en ese momento.largestShiftTime: cuándo ocurrió. Este campo es el que revela si tu CLS es un problema de carga o de contenido inyectado más tarde.largestShiftValue: la puntuación de ese desplazamiento concreto.largestShiftEntryylargestShiftSource: la entrada en crudo y la primera fuente, para cuando necesitas los rectángulos.loadState: igual que en INP, el estado del documento cuando ocurrió.
TTFB y FCP: el desglose de la conexión
TTFBAttribution desglosa el tiempo hasta el primer byte en cinco tramos consecutivos, y es la herramienta para saber si tu problema es de servidor o de red:
waitingDuration: desde que el usuario inicia la navegación hasta que la página empieza a atender la petición. Valores altos aquí son típicamente redirecciones.cacheDuration: tiempo comprobando la caché HTTP. En navegaciones gestionadas por un service worker suele incluir el arranque del worker y el proceso de sus escuchadores.dnsDuration: resolución del nombre.connectionDuration: creación de la conexión, incluido TLS.requestDuration: desde que se envía la petición hasta el primer byte de la respuesta. Incluye el tiempo de red y el de proceso del servidor.
FCPAttribution es más simple: timeToFirstByte, firstByteToFCP y loadState. La segunda cifra es la que interesa, porque un valor alto indica que el navegador está descargando muchos recursos que bloquean el renderizado o haciendo mucho trabajo antes de poder pintar.
Qué enviar y qué no
El conjunto mínimo que permite arreglar sin volver a mirar, sin desbordar el límite de tamaño:
import { onCLS, onINP, onLCP } from 'web-vitals/attribution';
function resumir(metrica) {
const base = {
nombre: metrica.name,
valor: Math.round(metrica.value * (metrica.name === 'CLS' ? 10000 : 1)),
id: metrica.id,
tipoNavegacion: metrica.navigationType,
};
const a = metrica.attribution;
switch (metrica.name) {
case 'LCP':
return { ...base,
elemento: a.target,
recurso: a.url,
ttfb: Math.round(a.timeToFirstByte),
retrasoCarga: Math.round(a.resourceLoadDelay),
duracionCarga: Math.round(a.resourceLoadDuration),
retrasoRender: Math.round(a.elementRenderDelay),
};
case 'INP':
return { ...base,
elemento: a.interactionTarget,
tipo: a.interactionType,
estadoCarga: a.loadState,
retrasoEntrada: Math.round(a.inputDelay),
procesamiento: Math.round(a.processingDuration),
presentacion: Math.round(a.presentationDelay),
scriptMasLargo: a.longestScript?.entry?.sourceURL,
};
case 'CLS':
return { ...base,
elemento: a.largestShiftTarget,
instante: Math.round(a.largestShiftTime),
valorMayor: Math.round(a.largestShiftValue * 10000),
estadoCarga: a.loadState,
};
}
}
Dos decisiones de este código merecen explicación. El CLS se multiplica por 10.000 y se redondea para almacenarlo como entero, lo cual evita problemas de coma flotante en la agregación y ahorra espacio; se divide al mostrar. Y nunca se envían los arrays de entradas: entries, longAnimationFrameEntries y processedEventEntries pueden ser enormes y hacer que sendBeacon falle en silencio por exceder su límite de tamaño.
Si no necesitas las entradas procesadas de INP ni siquiera en memoria, puedes ahorrarlas con la opción correspondiente:
onINP(resumirYEnviar, { includeProcessedEventEntries: false });
Personalizar cómo se identifican los elementos
Por defecto, los campos target generan un selector CSS. En una aplicación con clases generadas automáticamente, esos selectores son ilegibles e inestables entre despliegues. La opción generateTarget permite sustituirlos:
function objetivoPersonalizado(el) {
if (el?.dataset?.nombre) return el.dataset.nombre;
// Devolver undefined hace que se use el selector por defecto.
}
onLCP(enviar, { generateTarget: objetivoPersonalizado });
onINP(enviar, { generateTarget: objetivoPersonalizado });
onCLS(enviar, { generateTarget: objetivoPersonalizado });
Si la función devuelve null o undefined, o si no la pasas, se usa el selector por defecto. Marcar los elementos importantes con un atributo de datos estable convierte tus datos de atribución en algo agrupable y comparable entre versiones, que es exactamente lo que necesitas para detectar regresiones.
El fallo que convierte la atribución en ruido es guardar el selector tal cual llega y luego intentar agrupar. Un selector generado sobre una página con clases de utilidad puede ser distinto en cada visita por diferencias de contenido, y acabas con cincuenta mil valores únicos de los que no se puede sacar nada. La atribución solo produce información cuando puedes contar: “el elemento X es el LCP en el sesenta por ciento de las visitas malas”. Para poder contar necesitas identificadores estables entre visitas y entre despliegues, y eso hay que decidirlo antes de empezar a recoger, marcando con un atributo de datos los cinco o diez elementos que pueden ser el LCP de cada tipo de página y los controles principales con los que la gente interactúa. Son diez minutos de trabajo y la diferencia entre un panel que señala el culpable y una tabla de selectores irrepetibles que nadie vuelve a abrir.