Saltar al contenido

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.

6 min de lectura

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 imagenCuándo empieza la petición
<img> en el HTML inicial, eagerEn el escaneo de precarga, antes del CSS
<img> en el HTML inicial, lazyTras el layout, cuando el navegador sabe que está a la vista
Insertada por JavaScript tras la hidrataciónCuando 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

  1. Averigua qué elemento es el LCP, con datos de campo, ruta por ruta.
  2. Si es una imagen: priority en esa única imagen, sizes correcto, y asegúrate de que nada por encima en el HTML bloquee el parser.
  3. Si es texto: next/font con display: 'swap' y una fuente de respaldo con métricas cercanas.
  4. 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.

Preguntas relacionadas

¿El componente Image arregla el LCP por sí solo?
No. Arregla bytes y desplazamiento de maquetación. El LCP lo decide cuándo arranca la petición, así que un hero optimizado que el navegador descubre tarde sigue siendo un LCP lento.
¿La imagen hero debe llevar priority?
Sí, y solo ella. Marcar varias imágenes como priority vacía la palabra de significado y empuja la de verdad hacia atrás en la cola.
¿Las tipografías afectan al LCP?
Sí cuando el elemento mayor es texto. Una fuente que bloquea el pintado lo retrasa directamente, por eso font-display y precargar el corte que dibuja el titular forman parte de arreglar el LCP.
¿Basta con un CDN?
Hace que los bytes lleguen antes y no cambia el orden de descubrimiento. Una petición que empieza tarde empieza tarde también desde un servidor más cercano.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto