wandres.dev
MEDIR EN PRODUCCIÓN · La librería web-vitals y los percentiles

Instrumentar con la librería web-vitals

Cómo se monta la medición de campo con la librería oficial: las cinco funciones, los dos builds, el objeto que recibe la devolución de llamada, y los errores que producen datos falsos.

⏱ 18 min

Medir las Core Web Vitals a mano con las APIs del navegador es posible y es mala idea: cada métrica tiene entre cuatro y seis divergencias documentadas entre lo que reporta la API y lo que define la métrica, y reimplementarlas es garantía de datos que no comparan con nada. La librería web-vitals resuelve todas esas divergencias en unos dos kilobytes comprimidos, y usarla bien es cuestión de conocer cinco funciones y tres detalles.

🎯 Al terminar esta lección sabrás
  • Instrumentar las tres Core Web Vitals y las dos métricas de diagnóstico con la librería oficial.
  • Elegir entre el build estándar y el de atribución con criterio.
  • Interpretar cada campo del objeto que recibe la devolución de llamada.
  • Evitar los cuatro errores de uso que producen datos inutilizables.

Las cinco funciones

La librería expone una función por métrica, todas con la misma forma: reciben una devolución de llamada que se invoca cuando el valor está listo para reportarse.

import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';

onCLS(console.log);
onINP(console.log);
onLCP(console.log);
onFCP(console.log);
onTTFB(console.log);

Las tres primeras son las Core Web Vitals. onFCP y onTTFB son las métricas de diagnóstico que descomponen el LCP y que conviene enviar siempre, porque sin ellas no puedes distinguir un problema de servidor de uno de bloqueo de renderizado.

No existe onFID: la métrica desapareció del conjunto cuando INP la sustituyó, y la función se eliminó de la librería. Si encuentras código que la importa, es de otra época.

Un detalle de diseño que evita un error habitual: la librería usa el indicador buffered de PerformanceObserver, lo que le permite acceder a entradas de rendimiento anteriores a su propia carga. Es decir, no hace falta cargarla pronto. De hecho la recomendación oficial es la contraria: difiérela hasta después del código que afecta al usuario, porque su propio coste de carga y ejecución compite con lo que estás midiendo.

Cuándo se llama la devolución de llamada, y cuántas veces

Este es el punto que más confusión genera.

Puede no llamarse nunca. onINP no reporta si el usuario nunca interactúa. onCLS, onFCP y onLCP no reportan si la página se cargó en segundo plano.

Puede llamarse más de una vez. onCLS y onINP reportan cada vez que el estado de visibilidad de la página pasa a oculto, porque los navegadores a menudo no ejecutan devoluciones de llamada adicionales una vez que la página está en segundo plano. Y todas las métricas reportan de nuevo, con un identificador distinto, cuando la página se restaura desde la caché de retroceso y avance.

La consecuencia práctica es que tu sistema de análisis tiene que estar preparado para recibir varios valores de la misma métrica en la misma carga, y desduplicarlos o sumarlos por el campo id.

Existe una opción reportAllChanges que hace que la devolución se llame cada vez que el valor cambia. Es útil para depurar y no se recomienda en producción. Ojo con el matiz: reporta cuando cambia la métrica, no cuando llega una entrada nueva. Un desplazamiento de layout que no aumenta el CLS no dispara la devolución aunque tengas la opción activada, y una interacción que no supera a la peor tampoco.

🛑
No llames a estas funciones más de una vez por carga de página

Cada llamada a onCLS, onINP o cualquiera de las otras crea una instancia de PerformanceObserver y registra escuchadores de eventos que viven mientras viva la página. Llamarlas una vez es despreciable; llamarlas repetidamente, por ejemplo desde un componente que se monta en cada cambio de vista, acaba produciendo una fuga de memoria. Instrumenta una sola vez, en el arranque, fuera del ciclo de vida de los componentes.

Los dos builds

La librería se distribuye en dos versiones.

El build estándar pesa unos 2 KB comprimidos con Brotli y da el valor de cada métrica. Es lo mínimo para tener un panel.

El build de atribución añade alrededor de 1,5 KB y adjunta a cada métrica un objeto attribution con información de diagnóstico: qué elemento fue el LCP, qué interacción produjo el INP, qué elemento provocó el mayor desplazamiento, y el desglose por subpartes de LCP e INP. Se importa cambiando la ruta:

import { onCLS, onINP, onLCP } from 'web-vitals/attribution';

El uso es idéntico; lo único que cambia es que el objeto de métrica trae la propiedad extra.

La recomendación es clara: si vas a mirar los datos para arreglar algo, usa el build de atribución. Tres kilobytes y medio a cambio de saber qué elemento concreto está causando tu problema es un cambio excelente. El build estándar solo tiene sentido si únicamente quieres un panel de tendencia y aceptas hacer el diagnóstico por otra vía.

Además de las variantes de módulo ES, ambos builds se distribuyen en formato UMD y en formato de función autoejecutada, que exponen todo en el espacio de nombres global webVitals. Eso permite cargarlos con una etiqueta <script> clásica en sitios sin proceso de construcción.

El objeto de métrica

La devolución de llamada recibe un objeto con esta forma:

interface Metric {
  name: 'CLS' | 'FCP' | 'INP' | 'LCP' | 'TTFB';
  value: number;
  rating: 'good' | 'needs-improvement' | 'poor';
  delta: number;
  id: string;
  entries: PerformanceEntry[];
  navigationType:
    | 'navigate'
    | 'reload'
    | 'back-forward'
    | 'back-forward-cache'
    | 'prerender'
    | 'restore';
}

Campo a campo, con lo que importa de cada uno:

  • value es el valor actual de la métrica. Para CLS es una puntuación; para las demás, milisegundos.
  • rating es la clasificación ya calculada según los umbrales oficiales. Úsala en lugar de comparar tú contra constantes: si los umbrales cambian, la librería se actualiza y tu código no.
  • delta es la diferencia respecto al último valor reportado. La primera vez, delta y value coinciden. Es lo que necesitas si tu sistema de análisis no permite actualizar un valor ya enviado: envías el delta y sumas en destino agrupando por id.
  • id identifica esta instancia de la métrica. Sirve para desduplicar y para agrupar deltas. Cambia tras una restauración desde la caché de retroceso y avance, porque se considera una visita distinta.
  • entries son las entradas de rendimiento que sustentan el valor. Puede estar vacío, por ejemplo con un CLS de cero.
  • navigationType es oro puro para segmentar, y lo desarrollamos en la lección sobre segmentación.

La librería también exporta los umbrales como constantes, por si necesitas dibujarlos:

import { CLSThresholds, INPThresholds, LCPThresholds } from 'web-vitals';

console.log(CLSThresholds); // [0.1, 0.25]
console.log(INPThresholds); // [200, 500]
console.log(LCPThresholds); // [2500, 4000]

Soporte de navegadores y sus consecuencias

Aquí hay una asimetría que condiciona qué puedes concluir de tus datos.

Función Navegadores donde reporta
onCLS() Solo Chromium
onINP() Chromium, Firefox, Safari
onLCP() Chromium, Firefox, Safari
onFCP() Chromium, Firefox, Safari
onTTFB() Chromium, Firefox, Safari

El código de la librería se prueba en los tres motores y usa solo características ampliamente disponibles, así que no fallará en ninguno. Lo que ocurre es que las APIs subyacentes no existen en todos: el CLS solo lo vas a poder medir en Chromium. Si tu tráfico tiene una proporción alta de Safari, tu CLS de campo describe a una parte de tus usuarios, y conviene anotarlo en el panel para que nadie extrapole.

El error de instrumentación que más veces he visto y que invalida todo el panel sin que salte ninguna alarma

Cargar la librería dentro de un bloque condicional que depende del consentimiento de cookies. Parece lo correcto y produce un conjunto de datos silenciosamente inservible: los usuarios que rechazan el rastreo desaparecen de la muestra, y esos usuarios no son un subconjunto aleatorio. Suelen ser más técnicos, con más bloqueadores, con hardware distinto y con patrones de navegación distintos. Además, el propio banner de consentimiento es una de las mayores fuentes de desplazamiento de layout de la web, y al condicionar la medición a haberlo aceptado, estás excluyendo sistemáticamente el caso en que el usuario tarda en decidir, que es exactamente cuando el banner más daño hace. Las métricas de rendimiento no identifican a nadie y no necesitan consentimiento en la mayoría de marcos regulatorios si no las asocias a un identificador persistente. Instrumenta siempre, y si tienes dudas legales, quita el identificador de sesión antes que quitar la medición: un dato agregado sin identificador sigue sirviendo para todo lo que hemos descrito en este track.