next/image no va a arreglarte el LCP
El componente optimiza bytes. El LCP lo decide cuándo empieza la petición, y las causas habituales son el orden de descubrimiento, un hero en lazy y una fuente que bloquea el pintado.
Llegan equipos que ya han cambiado cada <img> por next/image, y el LCP no
se ha movido. Es el resultado esperable, y merece la pena entender por qué,
porque el mismo razonamiento señala lo que sí habría funcionado.
next/image optimiza la carga: un formato más pequeño, las dimensiones
adecuadas para el dispositivo, sin desplazamiento de layout. El LCP mide el
momento del pintado más grande. Los bytes son una entrada de eso, y
normalmente no la que manda.
Qué decide cuándo empieza la petición
El navegador solo puede pedir lo que ha descubierto. Para la imagen del hero hay tres posibilidades, y están separadas por un orden de magnitud:
| Cómo se encuentra la imagen | Cuándo empieza la petición |
|---|---|
<img> en el HTML inicial, eager | En el escaneo de precarga, antes del CSS |
<img> en el HTML inicial, lazy | Tras el layout, cuando el navegador sabe que está a la vista |
| Insertada por JavaScript tras la hidratación | Cuando el bundle se descarga, se parsea y se ejecuta |
next/image carga en lazy por defecto. Así que la migración típica
convierte un hero eager en uno lazy, retrasa su petición hasta después del
layout y empeora el LCP mientras todas las auditorías de imagen de Lighthouse
se ponen verdes.
El arreglo es una prop:
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="..."
width={1600}
height={900}
priority // eager + fetchpriority="high" + una pista de precarga
sizes="100vw"
className="w-full h-auto"
/>
);
}priority en exactamente una imagen por ruta: la que es el elemento LCP.
Ponerla en seis imágenes equivale a no ponerla en ninguna, porque todas compiten
por la misma conexión.
El error con sizes que cuesta más que el formato
sizes le dice al navegador qué candidato de srcset elegir, y se evalúa
antes del layout. Si te equivocas, descargas una imagen de 1600px para un
hueco de 400px o -peor para la calidad- una de 400px para un banner a sangre.
El valor por defecto al omitirlo es 100vw, que es correcto para un hero a
todo ancho e incorrecto para todo lo demás:
// Imagen de tarjeta en una rejilla de tres columnas.
<Image
src={post.cover}
alt=""
width={800}
height={450}
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
/>Ese único atributo suele valer más que la conversión a AVIF, porque cambia qué fichero se pide en lugar de cuántos bytes tiene ese fichero.
Cuando el elemento LCP es texto
En un sitio de contenido normalmente lo es, y entonces el trabajo con imágenes no viene al caso. El titular no puede pintarse hasta que se resuelve su fuente, así que la cadena es HTML → CSS → fichero de fuente → pintado.
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // pinta en la de respaldo y cambia cuando llega el fichero
variable: '--font-sans',
});next/font aloja el fichero en tu propio dominio y emite una precarga, lo que
saca la conexión de terceros de la cadena. display: 'swap' es lo que evita
que el titular espere: el precio es un cambio visible, que se reduce eligiendo
una fuente de respaldo con métricas parecidas, no bloqueando el pintado.
La otra mitad es la pila de respaldo. font-family: var(--font-sans), sans-serif
deja que el navegador pinte de inmediato con una fuente cuyas métricas no
elegiste; nombrar una del sistema que se parezca hace el cambio mucho menos
visible.
Medir lo que toca
Lighthouse reporta LCP de laboratorio en una carga fría y con el ancho de banda limitado. Los datos de campo reportan lo que vivieron tus visitantes, con aciertos de caché y dispositivos reales. Se contradicen constantemente, y el número de campo es el que cuenta.
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function Vitals() {
useReportWebVitals((metric) => {
if (metric.name !== 'LCP') return;
// El elemento, no solo el número: "el LCP es 3,1 s" no es accionable,
// "el LCP es 3,1 s y es la imagen del hero" sí lo es.
navigator.sendBeacon('/api/vitals', JSON.stringify({
value: metric.value,
element: metric.attribution?.element,
url: location.pathname,
}));
});
return null;
}Registra el elemento. La mitad de los proyectos de rendimiento que hacemos
empiezan descubriendo que el elemento LCP no es el que el equipo suponía: un
banner de cookies, un <h1> oculto, un contenedor vacío que resulta ser
grande.
El orden completo de qué arreglar y en qué secuencia está en Core Web Vitals en Next.js: qué mueve de verdad los números.
La versión corta
- Averigua qué elemento es el LCP, con datos de campo, ruta por ruta.
- Si es una imagen:
priorityen esa única imagen,sizescorrecto, y asegúrate de que nada por encima en el HTML bloquee el parser. - Si es texto:
next/fontcondisplay: 'swap'y una fuente de respaldo con métricas cercanas. - Solo después, piensa en formato y calidad.
Los pasos uno a tres son donde están los segundos. El paso cuatro es donde están las auditorías, que es justo por lo que se hace primero.
