Rendimiento de imágenes y fuentes — Optimiza imágenes y fuentes para LCP rápido y cero layout shift
Receta
// next/image — optimización automática, lazy loading, srcset
import Image from "next/image";
// next/font — auto-alojada, cero CLS, subset, peso variable
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
export default function HeroSection() {
return (
<section className={inter.className}>
<h1 className="text-5xl font-bold">¡Hola!</h1>
{/* Priority: precarga para LCP, sin lazy loading */}
<Image
src="/hero.jpg"
alt="Escaparate de productos"
width={1200}
height={600}
priority
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
sizes="100vw"
className="rounded-xl"
/>
</section>
);
}Cuándo usarlo: Siempre. Cada imagen debe usar next/image y cada fuente debe usar next/font. Estas no son optimizaciones para añadir después — son la línea base para cualquier app Next.js en producción.
Ejemplo funcional
// ---- ANTES: Imágenes y fuentes sin optimizar — LCP 4.2s, CLS 0.35 ----
// layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
{/* CDN de fuentes externo — bloquea renderizado, causa CLS */}
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body style={{ fontFamily: "Inter, sans-serif" }}>{children}</body>
</html>
);
}
// page.tsx
export default function HomePage() {
return (
<div>
{/* Sin optimizar: sin dimensiones, sin lazy loading, sin conversión de formato */}
<img src="/hero-original.png" alt="Hero" />
{/* PNG de 2.4MB, 3000x1500px, cargado eagerly incluso si está debajo del fold */}
<h1>Nuestros productos</h1>
<div className="grid grid-cols-3 gap-4">
{products.map((p) => (
<div key={p.id}>
{/* Sin width/height — causa layout shift */}
<img src={p.image} alt={p.name} />
<p>{p.name}</p>
</div>
))}
</div>
</div>
);
}
// ---- DESPUÉS: Optimizado — LCP 1.4s, CLS 0.02 ----
// layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
// Auto-alojada: sin solicitudes externas, sin CLS por cambio de fuente
// Automáticamente subconjunta solo los caracteres usados
// Fuente variable: un archivo cubre todos los pesos (ahorra ~100KB vs archivos separados)
});
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body className="font-sans">{children}</body>
</html>
);
}
// page.tsx
import Image from "next/image";
import heroImage from "@/public/hero.jpg"; // Importación estática para blur automático
export default function HomePage() {
return (
<div>
{/* Hero optimizado: precarga con priority, blur placeholder, srcset responsivo */}
<Image
src={heroImage}
alt="Escaparate de productos con nuestra última colección"
priority // Precarga para LCP — sin lazy loading
placeholder="blur" // Muestra versión borrosa instantáneamente (base64 inline)
sizes="100vw" // Imagen a ancho completo
quality={85} // Calidad ligeramente reducida — ahorra 30-40% de tamaño
className="w-full h-auto rounded-xl"
// Automático: conversión WebP/AVIF, srcset, lazy loading (excepto priority)
// PNG de 2.4MB -> 180KB WebP en tamaño apropiado
/>
<h1 className="text-4xl font-bold mt-8">Nuestros productos</h1>
<div className="grid grid-cols-3 gap-4 mt-6">
{products.map((p, index) => (
<div key={p.id}>
<Image
src={p.image}
alt={p.name}
width={400}
height={300}
// Primera fila visible al cargar — añadir priority
priority={index < 3}
placeholder="blur"
blurDataURL={p.blurDataURL}
sizes="(max-width: 768px) 100vw, 33vw"
className="rounded-lg"
/>
<p className="mt-2 font-medium">{p.name}</p>
</div>
))}
</div>
</div>
);
}Lo que esto demuestra:
- Imagen hero: PNG de 2.4MB a 180KB WebP (93% más pequeño), precargado para LCP
- Blur placeholder: los usuarios ven una vista previa borrosa instantáneamente en lugar de espacio en blanco
sizesresponsivo: sirve imagen de 400px en móvil en lugar de imagen de 1200px de escritorionext/font: Inter auto-alojada, peso variable, cero solicitudes externas, cero CLS- LCP: 4.2s a 1.4s (67% más rápido), CLS: 0.35 a 0.02 (94% reducción)
Análisis profundo
Cómo funciona
- Optimización automática de
next/image— Las imágenes se convierten a WebP o AVIF bajo demanda, se redimensionan a las dimensiones exactas necesarias, y se sirven con encabezadosCache-Controlapropiados. La optimización ocurre en tiempo de solicitud (o en tiempo de compilación para importaciones estáticas) en el servidor. - Generación de srcset —
next/imagegenera un atributosrcsetcon múltiples tamaños (640, 750, 828, 1080, 1200, 1920, 2048, 3840px por defecto). El navegador selecciona la imagen más pequeña que se ajuste al viewport, reduciendo bytes transferidos. - Lazy loading — Por defecto, las imágenes debajo del fold usan
loading="lazy". El navegador solo las obtiene cuando entran en el viewport. La propiedadprioritydesactiva el lazy loading y añade un<link rel="preload">para imágenes LCP. - Blur placeholder — Para importaciones estáticas, Next.js genera una diminuta (8x8) imagen borrosa en base64 en tiempo de compilación. Para imágenes dinámicas, tú proporcionas un
blurDataURL. El blur se muestra instantáneamente mientras la imagen completa carga, mejorando el rendimiento percibido. - Auto-alojamiento de
next/font— Las fuentes se descargan en tiempo de compilación y se sirven desde el mismo dominio que tu app. Esto elimina la búsqueda DNS, conexión TCP, y handshake TLS requeridos para solicitudes de CDN de fuentes externas (ahorra 100-300ms). - Fuentes variables — Un único archivo de fuente variable reemplaza múltiples archivos específicos de peso.
Intercomo fuente variable es ~100KB en lugar de 400KB+ para archivos separados de pesos 400, 500, 600, y 700. font-display: swap— El texto se renderiza inmediatamente con una fuente de respaldo, luego cambia a la fuente personalizada cuando carga. Combinado con ajuste de tamaño denext/font, esto produce CLS casi cero.
Variaciones
Fuentes personalizadas locales:
import localFont from "next/font/local";
const customFont = localFont({
src: [
{ path: "./fonts/custom-regular.woff2", weight: "400", style: "normal" },
{ path: "./fonts/custom-bold.woff2", weight: "700", style: "normal" },
],
display: "swap",
variable: "--font-custom",
});Imágenes remotas con generación de blur:
// Genera URL de datos blur en tiempo de compilación o solicitud
import { getPlaiceholder } from "plaiceholder";
async function ProductCard({ imageUrl }: { imageUrl: string }) {
const { base64 } = await getPlaiceholder(imageUrl);
return (
<Image
src={imageUrl}
alt="Producto"
width={400}
height={300}
placeholder="blur"
blurDataURL={base64}
/>
);
}Art direction con diferentes imágenes por breakpoint:
export function ResponsiveHero() {
return (
<picture>
<source media="(max-width: 768px)" srcSet="/hero-mobile.webp" />
<source media="(max-width: 1200px)" srcSet="/hero-tablet.webp" />
<Image
src="/hero-desktop.jpg"
alt="Hero"
width={1920}
height={800}
priority
sizes="100vw"
/>
</picture>
);
}Modo fill — la imagen llena su contenedor padre:
// Usa cuando no conoces dimensiones exactas, o la imagen debe
// estirarse/cubrir/contener su padre. El padre DEBE tener position + dimensiones.
export function AvatarCard({ src, name }: { src: string; name: string }) {
return (
<div className="relative h-64 w-64 overflow-hidden rounded-full">
<Image
src={src}
alt={name}
fill
sizes="256px"
className="object-cover" // cover, contain, o fill
/>
</div>
);
}Hero estilo background con fill + object-cover:
// Reemplaza CSS background-image — obtiene todas las optimizaciones de next/image
export function HeroBanner({ title }: { title: string }) {
return (
<section className="relative h-[60vh] w-full">
<Image
src="/hero-bg.jpg"
alt=""
fill
priority
sizes="100vw"
quality={80}
className="object-cover"
/>
{/* Overlay de contenido */}
<div className="relative z-10 flex h-full items-center justify-center">
<h1 className="text-5xl font-bold text-white drop-shadow-lg">{title}</h1>
</div>
</section>
);
}Grid responsivo con sizes correcto:
// sizes le dice al navegador qué variante de srcset descargar ANTES del layout.
// Sin él, el navegador por defecto es 100vw y descarga la variante de 3840px.
export function ProductGrid({ products }: { products: Product[] }) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{products.map((p, i) => (
<div key={p.id} className="relative aspect-square">
<Image
src={p.image}
alt={p.name}
fill
// Coincide con el grid: 1 col en móvil, 2 en sm, 4 en lg
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
priority={i < 4} // precarga solo primera fila
className="rounded-lg object-cover"
/>
</div>
))}
</div>
);
}Cargador de imagen personalizado (Cloudinary, Imgix, CDN personalizado):
import Image from "next/image";
// Cargador personalizado: next/image lo llama para generar la URL para cada entrada de srcset
function cloudinaryLoader({ src, width, quality }: { src: string; width: number; quality?: number }) {
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;
}
export function CloudinaryImage({ publicId, alt }: { publicId: string; alt: string }) {
return (
<Image
loader={cloudinaryLoader}
src={publicId}
alt={alt}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}
// O configura globalmente en next.config.ts:
// images: { loader: "custom", loaderFile: "./lib/image-loader.ts" }Imágenes SVG e íconos (omitir optimización):
// next/image optimiza imágenes raster (JPEG, PNG, WebP).
// Para SVGs, omite optimización — ya son vectoriales y pequeños.
export function Logo() {
return (
<Image
src="/logo.svg"
alt="Acme Inc"
width={120}
height={40}
unoptimized // Los SVGs no necesitan redimensionamiento o conversión de formato
/>
);
}
// Para SVGs inline con control de color, importa como componente React en lugar:
// import Logo from "./logo.svg"; // requiere @svgr/webpackGalería lazy-loaded debajo del fold con anulación de loading:
// Por defecto: lazy load de imágenes. Pero puedes ser explícito.
// Útil para documentación o al combinar con Intersection Observer.
export function Gallery({ images }: { images: string[] }) {
return (
<div className="columns-2 gap-4 lg:columns-3">
{images.map((src, i) => (
<Image
key={src}
src={src}
alt={`Imagen de galería ${i + 1}`}
width={600}
height={400}
loading="lazy" // explícito — igual que por defecto, pero intención clara
placeholder="blur"
blurDataURL="data:image/svg+xml;base64,..." // SVG shimmer diminuto
sizes="(max-width: 1024px) 50vw, 33vw"
className="mb-4 rounded-lg"
/>
))}
</div>
);
}Importaciones estáticas con blur automático (sin blurDataURL necesario):
// Cuando importas un archivo de imagen local, Next.js proporciona width, height,
// y blurDataURL automáticamente en tiempo de compilación. Sin valores manuales necesarios.
import productShot from "@/public/images/product-shot.jpg";
export function ProductHero() {
return (
<Image
src={productShot} // StaticImageData — incluye width, height, blur
alt="Foto de producto"
placeholder="blur" // blur funciona automáticamente para importaciones estáticas
priority
sizes="100vw"
className="w-full"
// Sin width, height, o blurDataURL necesarios — todos inferidos de la importación
/>
);
}Configuración de remotePatterns con protocolo y pathname:
// next.config.ts — control granular sobre fuentes de imágenes externas permitidas
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
pathname: "/images/**", // solo permitir ruta /images/
},
{
protocol: "https",
hostname: "*.unsplash.com", // wildcard de subdominio
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
],
// Anula los tamaños de dispositivo por defecto para generación de srcset
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// Anula los tamaños de imagen para la propiedad `sizes` (variantes más pequeñas)
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Formatos preferidos — Next.js intenta AVIF primero, luego WebP
formats: ["image/avif", "image/webp"],
},
};
export default nextConfig;Shimmer / placeholder de esqueleto (SVG personalizado):
// En lugar de una imagen borrosa, muestra un efecto shimmer animado
const shimmer = (w: number, h: number) => `
<svg width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="g">
<stop stop-color="#f6f7f8" offset="0%" />
<stop stop-color="#edeef1" offset="50%" />
<stop stop-color="#f6f7f8" offset="100%" />
</linearGradient>
</defs>
<rect width="${w}" height="${h}" fill="url(#g)" />
</svg>`;
function toBase64(str: string) {
return typeof window === "undefined"
? Buffer.from(str).toString("base64")
: window.btoa(str);
}
export function ShimmerImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={400}
height={300}
placeholder="blur"
blurDataURL={`data:image/svg+xml;base64,${toBase64(shimmer(400, 300))}`}
/>
);
}Reglas de next/image
- Cada imagen LCP obtiene
priority— Típicamente una por página. Sin ella, tu imagen hero hace lazy load y LCP sufre por 500ms+. - Cada imagen obtiene
sizes— Sinsizes, el navegador descarga la variante de 3840px para una miniatura de 300px. Haz coincidirsizescon tus breakpoints de grid/layout CSS. - Cada imagen obtiene
alt— Las imágenes decorativas obtienenalt=""(cadena vacía, no omitido). Las imágenes significativas obtienen texto alt descriptivo. - Usa
fillcuando dimensiones sean desconocidas — Avatares subidos por usuarios, imágenes de CMS con relaciones de aspecto variables. El padre debe tenerposition: relativey dimensiones explícitas. - Usa importaciones estáticas para imágenes locales — Obtienes
width,height, yblurDataURLautomáticos en tiempo de compilación. Sin valores manuales para mantener. - Configura
remotePatterns, nodomains—domainsestá deprecado.remotePatternssoporta wildcards y restricciones de ruta para seguridad. - Define
formats: ["image/avif", "image/webp"]— AVIF es 50% más pequeño que JPEG. Next.js sirve AVIF a navegadores que lo soportan y fallback a WebP. - Nunca uses
unoptimizeden imágenes raster — Solo úsalo para SVGs. Las imágenes raster (JPEG, PNG) siempre deben pasar por el pipeline de optimización. - Usa
quality={75-85}para fotos — Por defecto es 75. Aumenta a 80-85 para imágenes hero donde importa la calidad. Por debajo de 70, los artefactos JPEG se hacen visibles. - No hagas lazy load de imágenes por encima del fold — Las primeras 1-4 imágenes visibles al cargar la página deben tener
priority. Todo debajo del fold permanece lazy (por defecto).
Comparación de formato de imagen:
| Formato | Compresión | Soporte de navegador | Mejor para |
|---|---|---|---|
| JPEG | Bueno | Universal | Fotos, imágenes complejas |
| WebP | 25-35% más pequeño que JPEG | 97%+ navegadores | Opción por defecto para la mayoría de imágenes |
| AVIF | 50% más pequeño que JPEG | 92%+ navegadores | Compresión máxima cuando es soportado |
| PNG | Sin pérdida | Universal | Íconos, capturas de pantalla con texto |
| SVG | Vector | Universal | Logos, íconos, ilustraciones |
Notas de TypeScript
next/imageproporciona verificación de tipo completa para props incluyendosrc,width,height,alt.- Las importaciones estáticas (
import img from "./photo.jpg") se escriben comoStaticImageDataconwidth,height, yblurDataURLautomáticos. next/font/googleynext/font/localdevuelven objetos con propiedadesclassName,variable, ystyle.
Gotchas
-
Falta
priorityen imagen LCP — La imagen más grande visible (hero, foto de producto) por defecto hace lazy loading, demorando LCP. Arreglo: Añadeprioritya la imagen que es el elemento Largest Contentful Paint. Usualmente una por página. -
Atributo
sizesincorrecto — Sinsizes, el navegador asume que la imagen es 100vw y descarga la variante srcset más grande. Una imagen de tarjeta de 400px descarga en 3840px. Arreglo: Definesizespara coincidir con el ancho renderizado actual:sizes="(max-width: 768px) 100vw, 33vw". -
Imágenes externas sin configuración —
next/imagerechaza URLs externas a menos que estén configuradas. Arreglo: Añade dominios anext.config.ts:images: { remotePatterns: [{ hostname: "cdn.example.com" }] }. -
Falta width y height — Las imágenes sin dimensiones causan layout shift. El navegador no puede reservar espacio hasta que carga la imagen. Arreglo: Siempre proporciona
widthyheight, o usafillcon un contenedor padre posicionado. -
Flash de carga de fuente — Usar
@importo<link>para fuentes de Google Fonts causa un flash de texto sin estilo y CLS. Arreglo: Usanext/font/googleexclusivamente. Nunca añadas etiquetas<link>para Google Fonts. -
Demasiados pesos de fuente — Cargar 6+ pesos de fuente aumenta significativamente el tamaño total del archivo de fuente. Arreglo: Usa una fuente variable y limita a los pesos realmente usados en tu sistema de diseño (típicamente 400, 500, 700).
-
Usando
fillsin padre posicionado — La imagen se renderiza conposition: absolutey se desborda de su contenedor, cubriendo otro contenido. Arreglo: El padre debe tenerposition: relative(oabsolute/fixed) y ancho/alto explícito o relación de aspecto. -
Usando
domainsen lugar deremotePatterns—domainsestá deprecado y no soporta restricciones de ruta o wildcards. Arreglo: Cambia aremotePatternsconprotocol,hostname, ypathnamepara seguridad. -
Omitiendo
sizesen imágenes de grid/tarjeta — Una imagen de tarjeta de 25vw descarga la variante de 3840px (10x más grande que necesario). Arreglo: Siempre definesizespara coincidir con tu layout:"(max-width: 768px) 100vw, 25vw". -
Usando CSS
background-imageen lugar denext/image— Pierde optimización automática, lazy loading, srcset, y conversión de formato. Arreglo: Usafill+object-covercomo se muestra en la variación hero banner arriba.
Alternativas
| Enfoque | Ventaja/Desventaja |
|---|---|
next/image | Optimización automática; requiere Next.js |
| Cloudinary o Imgix | Optimización basada en CDN; dependencia externa y costo |
<img> con srcset manual | Control total; sin optimización automática |
CSS background-image | No puede usar next/image; pierde lazy loading y srcset |
next/font | Cero CLS, auto-alojada; solo Next.js |
| Fontsource | Paquetes npm auto-alojados; configuración manual |
| Fuentes variables vía CDN | Archivo único; aún tiene sobrecarga de solicitud externa |
FAQs
¿Qué hace exactamente la propiedad priority en next/image?
- Desactiva lazy loading para que la imagen cargue inmediatamente.
- Añade una etiqueta
<link rel="preload">en el<head>HTML para la imagen. - Solo debe usarse en la imagen LCP (típicamente una por página).
¿Cuánta reducción de tamaño proporciona next/image comparado con imágenes crudas?
- La conversión automática WebP/AVIF puede reducir un PNG de 2.4MB a ~180KB (93% más pequeño).
srcsetasegura que el navegador solo descargue el tamaño necesario para el viewport.- La propiedad
quality(por defecto 75) puede ajustarse para ahorros adicionales.
¿Por qué next/font elimina CLS de fuentes?
- Las fuentes se descargan en tiempo de compilación y se sirven desde el mismo dominio (sin solicitudes externas).
font-display: swapcombinado con ajuste automático de tamaño asegura layout shift casi cero.- Sin búsqueda DNS, conexión TCP, o handshake TLS para CDNs de fuentes externas.
¿Cuál es la diferencia entre una fuente variable y archivos de peso separado?
- Una fuente variable es un archivo único cubriendo todos los pesos (ej., 100-900).
- Los archivos separados requieren una descarga por peso (400, 500, 700, etc.).
- Las fuentes variables ahorran ~100KB+ comparado con cargar 4+ archivos de peso separado.
¿Cómo funcionan los placeholders borrosos para imágenes estáticas vs dinámicas?
- Importaciones estáticas: Next.js genera automáticamente un blur diminuto (8x8) en base64 en tiempo de compilación.
- Imágenes dinámicas: Debes proporcionar un
blurDataURL(ej., vía la libreríaplaiceholder). - El blur se muestra instantáneamente mientras carga la imagen completa, mejorando el rendimiento percibido.
Gotcha: ¿Qué pasa si omites el atributo sizes en next/image?
- Sin
sizes, el navegador asume que la imagen es100vwde ancho. - Descarga la variante
srcsetmás grande (hasta 3840px) incluso para una imagen de tarjeta de 400px. - Arreglo: Define
sizespara coincidir con el ancho renderizado actual, ej.,sizes="(max-width: 768px) 100vw, 33vw".
¿Cómo configuras next/image para URLs de imagen externas (remotas)?
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{ hostname: "cdn.example.com" },
{ hostname: "images.unsplash.com" },
],
},
};Gotcha: ¿Por qué nunca deberías usar una etiqueta de enlace de Google Fonts en una app Next.js?
- Las etiquetas
<link>externas para fuentes bloquean el renderizado y causan un flash de texto sin estilo. - La solicitud externa añade latencia de DNS + TCP + TLS (100-300ms).
- Arreglo: Usa
next/font/googleexclusivamente -- auto-aloja la fuente en tiempo de compilación.
¿Cómo se escribe StaticImageData cuando importas una imagen en TypeScript?
- Las importaciones estáticas como
import hero from "./hero.jpg"se escriben comoStaticImageData. - El tipo incluye
src,width,height, yblurDataURLautomáticamente. - Obtienes errores de compilación si pasas props inválidos a
<Image>.
¿Qué tipo devuelve next/font/google y cómo lo usas en TypeScript?
import { Inter } from "next/font/google";
// Devuelve { className: string; variable: string; style: { fontFamily: string } }
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
// Usa className en elementos o variable en <html> para acceso a variable CSS
<html className={inter.variable}>¿Cuándo debes usar la propiedad fill en lugar de width y height en next/image?
- Usa
fillcuando la imagen debe estirarse para llenar su contenedor padre. - El padre debe tener
position: relativey dimensiones definidas. - Útil para banners hero o layouts responsivos donde dimensiones exactas varían en píxeles.
¿Cómo configuras una fuente personalizada local con next/font/local?
import localFont from "next/font/local";
const customFont = localFont({
src: [
{ path: "./fonts/custom-regular.woff2", weight: "400" },
{ path: "./fonts/custom-bold.woff2", weight: "700" },
],
display: "swap",
variable: "--font-custom",
});Relacionado
- Core Web Vitals — Objetivos LCP y CLS que imágenes y fuentes impactan directamente
- Rendimiento de Server Components — Componentes de imagen como Server Components
- Optimización de Bundle — Archivos de fuentes como parte del presupuesto general de bundle
- Checklist de Rendimiento — Items de auditoría de imágenes y fuentes