Las Core Web Vitals se explican mal casi siempre. Se habla de puntuaciones y no de decisiones. Esto es lo que aplicamos en producción, en el orden en que mueve la aguja.
LCP: el elemento grande, no el sitio entero
El Largest Contentful Paint mide cuándo aparece el elemento más grande visible al cargar. En una web de agencia suele ser el titular o la imagen del hero. Tres decisiones se llevan casi todo el resultado:
- Fuentes con
next/font. Se autoalojan en el build, se precargan y se les asignadisplay: swap. Se acabaron las peticiones a un dominio externo antes de poder pintar texto. - Una sola imagen con
priority. La del hero y ninguna más. Todo lo demás va conloading="lazy". Ponerpriorityen media docena de imágenes es la forma más rápida de empeorar el LCP. - Vídeo de fondo que no compite. El póster se sirve como imagen normal y el vídeo se monta después del primer pintado. El LCP lo marca el póster o el titular, nunca la descarga del
.mp4.
Si el LCP sigue alto después de esto, el problema casi siempre está en el servidor: tiempo de respuesta, no de pintado.
CLS: reservar el hueco antes de tenerlo
El Cumulative Layout Shift mide cuánto se mueve el contenido después de aparecer. Las causas son siempre las mismas:
- Imágenes sin dimensiones. Con
next/imageyfilldentro de un contenedor conaspect-ratioel hueco está reservado desde el primer frame. - Fuentes que cambian de métrica al cargar.
next/fontgenera un fallback ajustado y el salto desaparece. - Banners y avisos que se insertan arriba del todo. Si hay que mostrarlos, van en
position: fixed, fuera del flujo.
INP: el coste de responder
El Interaction to Next Paint sustituyó al FID y es mucho más exigente: mide el retraso entre la interacción y el siguiente pintado, incluyendo el trabajo del hilo principal. Lo que nos funciona:
- Animar solo
transformyopacity. Son propiedades de compositor: no provocan layout ni paint. Animarwidth,height,topoleftobliga al navegador a recalcular en cada frame y se come el presupuesto justo cuando el usuario hace clic. - Librerías de animación con
import()dinámico. GSAP y ScrollTrigger pesan más que el resto del bundle de una landing. Si solo se usan en dos secciones, se cargan en esas dos secciones y después del primer pintado. - Nada de escuchar
scrollsinrequestAnimationFrame. Un listener que leegetBoundingClientRect()en cada evento fuerza reflow decenas de veces por segundo.
El presupuesto de rendimiento
Una regla simple evita discusiones: cada dependencia nueva tiene que justificar su peso. Antes de instalar, se mira cuánto añade al bundle del cliente y qué componente la necesita. Si solo la usa una sección, se carga solo ahí.
En nuestros proyectos el presupuesto es explícito: JavaScript de primera carga por debajo de 120 KB comprimido, LCP por debajo de 2 segundos en 4G simulada y CLS por debajo de 0,05. Si un cambio lo rompe, no se despliega.
Qué medir después de desplegar
Lighthouse en local es una simulación. Los datos que cuentan son los de campo, los que Google recoge de usuarios reales (CrUX). Un sitio puede sacar 100 en Lighthouse y tener malas métricas reales si el servidor va lento en hora punta o si el 60 % del tráfico entra desde móviles antiguos.
Por eso conectamos analítica de Web Vitals desde el primer día: los percentiles 75 de LCP, CLS e INP se revisan cada semana durante el primer mes. Ahí es donde aparecen los problemas que ninguna auditoría en local enseña.