Core Web Vitals en Next.js: qué mueve de verdad los números
LCP, INP y CLS tienen causas concretas en una aplicación Next.js. Esta es la lista ordenada que recorremos en los proyectos de rendimiento, con los cambios que sí pagan.
Una puntuación de Lighthouse es una medición de laboratorio de una carga de página en un dispositivo simulado. Los datos de campo son lo que tus visitantes experimentaron de verdad, y son lo que usa Google. Empieza siempre por ahí:
# p75 field data for an origin, from the Chrome UX Report
curl -s "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$CRUX_KEY" \
-H 'Content-Type: application/json' \
-d '{"origin":"https://example.com","formFactor":"PHONE"}'Si tu puntuación de laboratorio es 95 y tu LCP de campo es 3,8 s, el laboratorio te está mintiendo sobre algo — normalmente una fuente, un script de terceros, o una red real que la simulada no está modelando.
LCP: casi siempre es la imagen o la fuente
Largest Contentful Paint mide cuándo termina de pintarse el elemento más grande visible sin desplazar. En una aplicación Next.js, cuatro causas cubren la gran mayoría de los fallos.
1. La imagen hero no está priorizada
import Image from 'next/image';
<Image
src="/hero.jpg"
alt=""
width={1200}
height={630}
priority // preloads it; without this it waits in the lazy queue
sizes="100vw"
quality={80}
/>Sin priority, tu elemento LCP se queda detrás de todos los demás recursos en
la cola del navegador. Es un cambio de una línea que habitualmente quita medio
segundo al LCP móvil.
Igualmente: poner priority en más de una o dos imágenes es el mismo error al
revés — si todo se precarga, nada está priorizado.
2. La ruta es dinámica sin necesitarlo
Una ruta renderizada estáticamente sirve HTML desde el nodo edge más cercano. Una dinámica ejecuta primero tu árbol de componentes y tu base de datos. Si el tiempo hasta el primer byte supera los 600 ms, ninguna optimización de imagen salvará el LCP — el navegador todavía no ha recibido nada que pintar.
Esa es la conexión entre estrategia de renderizado y Core Web Vitals que la mayoría de las listas de comprobación de rendimiento se saltan por completo.
3. Las fuentes bloquean el pintado
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // paint immediately with the fallback
preload: true,
adjustFontFallback: true, // reduces the shift when the real font arrives
});next/font aloja el fichero en tu propio origen, lo que elimina una
resolución DNS, un handshake TLS y un viaje de ida y vuelta a un origen de
terceros de tu camino crítico. Si aún cargas fuentes con un <link> a un CDN
de fuentes, ahí tienes una mejora gratis sobre la mesa.
4. Hay un script de terceros en el head
Gestores de etiquetas, widgets de chat, banners de consentimiento, grabadores de sesión. Cada uno es una petición bloqueante en el camino crítico salvo que se difiera explícitamente:
import Script from 'next/script';
<Script src="https://example.com/widget.js" strategy="lazyOnload" />En las auditorías, el mayor contribuyente individual al LCP es un script de terceros aproximadamente un tercio de las veces. Conviene medirlo antes de gastar un sprint en tu propio código.
INP: el coste de la hidratación
Interaction to Next Paint sustituyó a First Input Delay porque FID medía lo que no tocaba — cronometraba solo el retraso antes de que arrancara el manejador, no cuánto esperó la visitante para ver un resultado.
Los problemas de INP en Next.js son problemas de bundle. Las causas, en el orden en que las encontramos:
- Un límite de cliente demasiado arriba en el árbol. Lo tratamos a fondo
en nuestro artículo sobre dónde te cuesta de verdad
use client. - Una dependencia pesada importada en el ámbito del módulo. Librerías de
gráficos, de fechas, editores. Cárgalas con
next/dynamicyssr: falsecuando el componente esté realmente por debajo del pliegue. - Trabajo caro en un manejador de eventos. Filtrar diez mil filas en un
onChange. Llévalo al servidor, o aplica debounce y virtualiza. - Demasiadas raíces de hidratación. Cien tarjetas interactivas independientes en una página de listado son cien unidades de hidratación compitiendo por el hilo principal.
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('@/components/Chart'), {
ssr: false,
loading: () => <ChartSkeleton />,
});CLS: reserva el espacio
Cumulative Layout Shift es el más fácil de arreglar de los tres y el más vergonzoso de publicar.
- Da siempre
widthyheightexplícitos a las imágenes, o envuélvelas en un contenedor conaspect-ratiofijo.next/imagelo impone, que es una de las mejores razones para usarlo. - Reserva espacio para todo lo que llega tarde — huecos publicitarios, incrustaciones, banners de consentimiento. Un esqueleto con las dimensiones del elemento final no cuesta nada y elimina el desplazamiento por completo.
- Usa
font-display: swapjunto conadjustFontFallbackpara que las métricas de la fuente de reserva se aproximen a las de la real. - Nunca inyectes un banner encima del contenido existente después de cargar. Superponlo, o reserva el espacio en el HTML inicial.
Defender el resultado
Cada arreglo de arriba se degrada. Alguien añade una dependencia, alguien añade un script, alguien mueve un límite. Sin un mecanismo que lo imponga, volverás a hacer este trabajo el año que viene.
# .github/workflows/performance.yml
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
https://staging.example.com/
https://staging.example.com/pricing
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true[
{
"path": "/*",
"resourceSizes": [
{ "resourceType": "script", "budget": 180 },
{ "resourceType": "total", "budget": 600 }
],
"timings": [{ "metric": "interactive", "budget": 3000 }]
}
]Una pull request que empuja el presupuesto de JavaScript por encima de 180 KB ahora hace fallar una comprobación en lugar de publicarse en silencio. Ese fichero de presupuesto es el entregable real de un proyecto de rendimiento. La puntuación mejorada es solo lo que ocurre de camino a tenerlo.
El orden en que trabajamos
- Conseguir datos de campo. Segmentar por clase de dispositivo y plantilla de ruta.
- Arreglar la estrategia de renderizado — una ruta dinámica que debería ser estática gana a todo lo demás de esta lista.
- Arreglar el elemento LCP: prioridad, dimensiones, formato.
- Pasar las fuentes a
next/fonty sacar los scripts de terceros del camino crítico. - Bajar el límite de cliente; dividir en código lo que quede.
- Reservar espacio para todo lo que llegue tarde.
- Poner un presupuesto en CI para que nada de lo anterior pueda retroceder.
Los pasos dos y tres suelen ser la mayor parte de la ganancia. El resto es conservarla.
