Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// 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.
// ---- 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:
sizes responsivo: 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 CLSnext/image - Las imágenes se convierten a WebP o AVIF bajo demanda, se redimensionan a las dimensiones exactas necesarias, y se sirven con encabezados Cache-Control apropiados. La optimización ocurre en tiempo de solicitud (o en tiempo de compilación para importaciones estáticas) en el servidor.next/image genera un atributo srcset con 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.loading="lazy". El navegador solo las obtiene cuando entran en el viewport. La propiedad priority desactiva el lazy loading y añade un <link rel="preload"> para imágenes LCP.blurDataURL. El blur se muestra instantáneamente mientras la imagen completa carga, mejorando el rendimiento percibido.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).Inter como 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 de next/font, esto produce CLS casi cero.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))}`}
/>
);
}priority - Típicamente una por página. Sin ella, tu imagen hero hace lazy load y LCP sufre por 500ms+.sizes - Sin sizes, el navegador descarga la variante de 3840px para una miniatura de 300px. Haz coincidir sizes con tus breakpoints de grid/layout CSS.alt - Las imágenes decorativas obtienen alt="" (cadena vacía, no omitido). Las imágenes significativas obtienen texto alt descriptivo.fill cuando dimensiones sean desconocidas - Avatares subidos por usuarios, imágenes de CMS con relaciones de aspecto variables. El padre debe tener position: relative y dimensiones explícitas.width, height, y blurDataURL automáticos en tiempo de compilación. Sin valores manuales para mantener.remotePatterns, no domains - domains está deprecado. remotePatterns soporta wildcards y restricciones de ruta para seguridad.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.unoptimized en imágenes raster - Solo úsalo para SVGs. Las imágenes raster (JPEG, PNG) siempre deben pasar por el pipeline de optimización.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.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 |
next/image proporciona verificación de tipo completa para props incluyendo src, width, height, alt.import img from "./photo.jpg") se escriben como StaticImageData con width, height, y blurDataURL automáticos.next/font/google y next/font/local devuelven objetos con propiedades className, variable, y style.Falta priority en imagen LCP - La imagen más grande visible (hero, foto de producto) por defecto hace lazy loading, demorando LCP. Arreglo: Añade priority a la imagen que es el elemento Largest Contentful Paint. Usualmente una por página.
Atributo sizes incorrecto - Sin sizes, 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: Define sizes para coincidir con el ancho renderizado actual: sizes="(max-width: 768px) 100vw, 33vw".
Imágenes externas sin configuración - next/image rechaza URLs externas a menos que estén configuradas. Arreglo: Añade dominios a next.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 width y height, o usa fill con un contenedor padre posicionado.
Flash de carga de fuente - Usar @import o <link> para fuentes de Google Fonts causa un flash de texto sin estilo y CLS. Arreglo: Usa next/font/google exclusivamente. 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 fill sin padre posicionado - La imagen se renderiza con position: absolute y se desborda de su contenedor, cubriendo otro contenido. Arreglo: El padre debe tener position: relative (o absolute/fixed) y ancho/alto explícito o relación de aspecto.
Usando domains en lugar de remotePatterns - domains está deprecado y no soporta restricciones de ruta o wildcards. Arreglo: Cambia a remotePatterns con protocol, hostname, y pathname para seguridad.
Omitiendo sizes en imágenes de grid/tarjeta - Una imagen de tarjeta de 25vw descarga la variante de 3840px (10x más grande que necesario). Arreglo: Siempre define sizes para coincidir con tu layout: "(max-width: 768px) 100vw, 25vw".
Usando CSS background-image en lugar de next/image - Pierde optimización automática, lazy loading, srcset, y conversión de formato. Arreglo: Usa fill + object-cover como se muestra en la variación hero banner arriba.
| 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 |
<link rel="preload"> en el <head> HTML para la imagen.srcset asegura que el navegador solo descargue el tamaño necesario para el viewport.quality (por defecto 75) puede ajustarse para ahorros adicionales.font-display: swap combinado con ajuste automático de tamaño asegura layout shift casi cero.blurDataURL (ej., vía la librería plaiceholder).sizes, el navegador asume que la imagen es 100vw de ancho.srcset más grande (hasta 3840px) incluso para una imagen de tarjeta de 400px.sizes para coincidir con el ancho renderizado actual, ej., sizes="(max-width: 768px) 100vw, 33vw".// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{ hostname: "cdn.example.com" },
{ hostname: "images.unsplash.com" },
],
},
};<link> externas para fuentes bloquean el renderizado y causan un flash de texto sin estilo.next/font/google exclusivamente -- auto-aloja la fuente en tiempo de compilación.import hero from "./hero.jpg" se escriben como StaticImageData.src, width, height, y blurDataURL automáticamente.<Image>.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}>fill cuando la imagen debe estirarse para llenar su contenedor padre.position: relative y dimensiones definidas.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",
});Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥