Optimización de Imágenes
Las imágenes sin optimizar son la causa #1 de sitios web lentos. NextJS incluye un componente Image
que optimiza automáticamente tus imágenes.
El problema con <img>
// ❌ Tag HTML tradicional
<img src="/producto.jpg" alt="Producto" />
Problemas:
- Se carga en tamaño original (5 MB en vez de 50 KB)
- No es responsive (misma imagen en móvil y desktop)
- No usa formatos modernos (WebP, AVIF)
- No tiene lazy loading (carga todas las imágenes al inicio)
- Causa Layout Shift (la página "salta" cuando carga)
Resultado: Sitio lento, mal puntaje en Google, usuarios frustrados.
La solución: <Image>
import Image from 'next/image'
// ✅ Componente de NextJS
<Image
src="/producto.jpg"
alt="Producto"
width={500}
height={300}
/>
NextJS automáticamente:
- ✅ Optimiza el tamaño (reduce 5 MB a 50 KB)
- ✅ Genera múltiples tamaños (responsive)
- ✅ Convierte a WebP/AVIF
- ✅ Lazy loading (carga cuando es visible)
- ✅ Previene Layout Shift
- ✅ Cachea agresivamente
Uso básico
Imágenes locales
import Image from 'next/image'
import productoImg from '@/public/producto.jpg'
export default function ProductoCard() {
return (
<Image
src={productoImg}
alt="Camiseta azul"
// width y height opcionales con imports
/>
)
}
Con import: NextJS conoce el tamaño automáticamente.
Imágenes con path
<Image
src="/images/producto.jpg"
alt="Camiseta azul"
width={500}
height={300}
// width y height REQUERIDOS con paths
/>
Con path: Debes especificar width y height.
Imágenes externas
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'api.mitienda.com',
port: '',
pathname: '/images/**',
},
],
},
}
<Image
src="https://api.mitienda.com/images/producto.jpg"
alt="Producto"
width={500}
height={300}
/>
Seguridad
Debes autorizar dominios externos en next.config.js
. Esto previene que alguien abuse de tu servidor de optimización.
Propiedades principales
fill (imágenes responsivas)
Para imágenes que deben llenar su contenedor:
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/producto.jpg"
alt="Producto"
fill
style={{ objectFit: 'cover' }}
/>
</div>
Cuándo usar:
- Imágenes de fondo
- Hero sections
- Imágenes que cambian de tamaño según el viewport
sizes (responsive images)
Indica a NextJS qué tamaño tendrá la imagen en diferentes viewports:
<Image
src="/producto.jpg"
alt="Producto"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
Qué significa:
- Móvil (≤768px): 100% del ancho de viewport
- Tablet (≤1200px): 50% del ancho
- Desktop (>1200px): 33% del ancho
NextJS genera y sirve el tamaño apropiado para cada caso.
priority
Para imágenes "above the fold" (visibles inmediatamente):
// Hero image
<Image
src="/hero.jpg"
alt="Banner principal"
width={1920}
height={1080}
priority
/>
Sin priority: Lazy loading (carga cuando llegas a ella) Con priority: Precarga inmediatamente, sin lazy loading
Usa priority en:
- Hero images
- Logos principales
- Primera imagen visible
- LCP (Largest Contentful Paint)
quality
Controla la calidad de compresión (1-100):
<Image
src="/producto.jpg"
alt="Producto"
width={500}
height={300}
quality={75} // Default: 75
/>
Recomendaciones:
90-100
: Fotografía profesional, portfolios75-85
: E-commerce, blogs (default: 75)60-70
: Thumbnails, imágenes pequeñas
placeholder
Muestra algo mientras la imagen carga:
// Blur placeholder
<Image
src="/producto.jpg"
alt="Producto"
width={500}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // Base64 tiny
/>
Con imports locales:
import productoImg from '@/public/producto.jpg'
<Image
src={productoImg}
alt="Producto"
placeholder="blur" // Se genera automáticamente
/>
Ejemplos prácticos
Hero section
import Image from 'next/image'
import heroImg from '@/public/hero.jpg'
export default function Hero() {
return (
<section className="relative h-screen">
<Image
src={heroImg}
alt="Bienvenido a nuestra tienda"
fill
priority
quality={90}
placeholder="blur"
style={{ objectFit: 'cover' }}
/>
<div className="relative z-10 flex items-center justify-center h-full">
<h1 className="text-6xl font-bold text-white">
Bienvenidos
</h1>
</div>
</section>
)
}
Card de producto
import Image from 'next/image'
export default function ProductoCard({ producto }) {
return (
<article className="border rounded-lg overflow-hidden">
<div className="relative aspect-square">
<Image
src={producto.imagen}
alt={producto.nombre}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ objectFit: 'cover' }}
className="hover:scale-105 transition-transform"
/>
{producto.descuento && (
<span className="absolute top-2 right-2 bg-red-500 text-white px-2 py-1 rounded">
-{producto.descuento}%
</span>
)}
</div>
<div className="p-4">
<h3 className="font-bold text-lg">{producto.nombre}</h3>
<p className="text-2xl text-blue-600">${producto.precio}</p>
</div>
</article>
)
}
Grid de productos
export default async function ProductosPage() {
const productos = await fetch('https://api.mitienda.com/productos')
.then(r => r.json())
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Productos</h1>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
{productos.map((producto) => (
<ProductoCard key={producto.id} producto={producto} />
))}
</div>
</div>
)
}
Galería con lightbox
'use client'
import { useState } from 'react'
import Image from 'next/image'
export default function Galeria({ imagenes }) {
const [selected, setSelected] = useState(null)
return (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{imagenes.map((img, i) => (
<div
key={i}
className="relative aspect-square cursor-pointer"
onClick={() => setSelected(img)}
>
<Image
src={img}
alt={`Imagen ${i + 1}`}
fill
sizes="(max-width: 768px) 50vw, 25vw"
style={{ objectFit: 'cover' }}
className="rounded hover:opacity-80 transition"
/>
</div>
))}
</div>
{/* Lightbox */}
{selected && (
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
onClick={() => setSelected(null)}
>
<div className="relative w-full h-full max-w-4xl max-h-4xl">
<Image
src={selected}
alt="Imagen ampliada"
fill
quality={95}
style={{ objectFit: 'contain' }}
/>
</div>
</div>
)}
</>
)
}
Avatar de usuario
import Image from 'next/image'
type AvatarProps = {
src: string
alt: string
size?: 'sm' | 'md' | 'lg'
}
export default function Avatar({ src, alt, size = 'md' }: AvatarProps) {
const sizes = {
sm: 32,
md: 48,
lg: 64,
}
const dimension = sizes[size]
return (
<div className="relative rounded-full overflow-hidden" style={{ width: dimension, height: dimension }}>
<Image
src={src}
alt={alt}
fill
sizes={`${dimension}px`}
style={{ objectFit: 'cover' }}
/>
</div>
)
}
Formatos de imagen
NextJS automáticamente sirve WebP o AVIF si el navegador lo soporta:
JPEG original: 500 KB → WebP: 150 KB (70% más pequeño) → AVIF: 100 KB (80% más pequeño)
No necesitas hacer nada, NextJS lo maneja automáticamente.
Forzar formato
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
},
}
Lazy loading
Por defecto, todas las imágenes tienen lazy loading (excepto las con priority
).
// ✅ Lazy loading automático
<Image src="/imagen.jpg" alt="Producto" width={500} height={300} />
// ❌ Sin lazy loading
<Image src="/imagen.jpg" alt="Hero" width={500} height={300} priority />
Qué es lazy loading:
- Las imágenes solo se cargan cuando están cerca de ser visibles
- Ahorra ancho de banda
- Página inicial más rápida
Prevenir Layout Shift
Layout Shift es cuando la página "salta" al cargar imágenes.
Con width/height
// ✅ Bien: NextJS reserva espacio
<Image
src="/producto.jpg"
alt="Producto"
width={500}
height={300}
/>
Con aspect ratio
// ✅ Bien con fill
<div className="relative aspect-video">
<Image
src="/producto.jpg"
alt="Producto"
fill
style={{ objectFit: 'cover' }}
/>
</div>
Aspect ratios comunes
// Square (1:1)
<div className="relative aspect-square">
// Video (16:9)
<div className="relative aspect-video">
// Portrait (3:4)
<div className="relative aspect-[3/4]">
// Custom
<div className="relative aspect-[4/3]">
Loaders personalizados
Para usar CDNs de imágenes:
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/imageLoader.js',
},
}
// lib/imageLoader.js
export default function cloudinaryLoader({ src, width, quality }) {
const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`]
return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`
}
Configuración avanzada
Tamaños de dispositivos
// next.config.js
module.exports = {
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
NextJS genera imágenes en estos tamaños.
Dominios permitidos (legacy)
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
},
{
protocol: 'https',
hostname: 'cdn.mitienda.com',
pathname: '/uploads/**',
},
],
},
}
Desactivar optimización
// next.config.js
module.exports = {
images: {
unoptimized: true, // No optimizar (no recomendado)
},
}
Solo usa esto si despliegas en un hosting que no soporta optimización de imágenes.
Performance
Medir impacto
Usa Lighthouse para medir:
# Chrome DevTools → Lighthouse
# Métricas importantes:
# - LCP (Largest Contentful Paint)
# - CLS (Cumulative Layout Shift)
# - Speed Index
Sin Image: LCP = 4s, CLS = 0.3 Con Image: LCP = 1.2s, CLS = 0
Tips de performance
- Usa
priority
solo en la primera imagen visible
// ✅ Bien
<Image src="/hero.jpg" priority /> // Solo el hero
// ❌ Mal
<Image src="/producto1.jpg" priority />
<Image src="/producto2.jpg" priority />
<Image src="/producto3.jpg" priority /> // Todas priority = ninguna priority
- Define
sizes
correctamente
// ❌ Mal: NextJS carga imagen de 1920px en móvil
<Image src="/img.jpg" width={1920} height={1080} />
// ✅ Bien: Carga tamaño apropiado por device
<Image
src="/img.jpg"
width={1920}
height={1080}
sizes="(max-width: 768px) 100vw, 50vw"
/>
- Comprime imágenes antes de subirlas
Herramientas:
- TinyPNG - Compresión online
- ImageOptim - Mac app
- Squoosh - Web app de Google
NextJS optimiza, pero partir de imágenes más pequeñas es mejor.
Imágenes desde CMS
Con Contentful
import Image from 'next/image'
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<Image
src={`https:${post.coverImage.url}`}
alt={post.coverImage.description}
width={post.coverImage.width}
height={post.coverImage.height}
priority
/>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Con Cloudinary
import Image from 'next/image'
const cloudinaryLoader = ({ src, width, quality }) => {
return `https://res.cloudinary.com/tu-cloud/image/upload/w_${width},q_${quality || 'auto'}/${src}`
}
export default function Producto({ producto }) {
return (
<Image
loader={cloudinaryLoader}
src={producto.imagenId}
alt={producto.nombre}
width={500}
height={300}
/>
)
}
Patrones comunes
Imagen con skeleton
'use client'
import { useState } from 'react'
import Image from 'next/image'
export default function ProductoImagen({ src, alt }) {
const [loading, setLoading] = useState(true)
return (
<div className="relative aspect-square">
{loading && (
<div className="absolute inset-0 bg-gray-200 animate-pulse" />
)}
<Image
src={src}
alt={alt}
fill
style={{ objectFit: 'cover' }}
onLoadingComplete={() => setLoading(false)}
/>
</div>
)
}
Imagen con fallback
'use client'
import { useState } from 'react'
import Image from 'next/image'
export default function ProductoImagen({ src, alt }) {
const [error, setError] = useState(false)
if (error) {
return (
<div className="aspect-square bg-gray-200 flex items-center justify-center">
<span className="text-gray-400">Sin imagen</span>
</div>
)
}
return (
<Image
src={src}
alt={alt}
width={500}
height={500}
onError={() => setError(true)}
/>
)
}
Galería con navegación
'use client'
import { useState } from 'react'
import Image from 'next/image'
export default function ProductoGaleria({ imagenes }) {
const [actual, setActual] = useState(0)
return (
<div>
{/* Imagen principal */}
<div className="relative aspect-square mb-4">
<Image
src={imagenes[actual]}
alt={`Imagen ${actual + 1}`}
fill
priority
quality={90}
style={{ objectFit: 'cover' }}
/>
</div>
{/* Thumbnails */}
<div className="grid grid-cols-4 gap-2">
{imagenes.map((img, i) => (
<button
key={i}
onClick={() => setActual(i)}
className={`relative aspect-square ${
i === actual ? 'ring-2 ring-blue-500' : ''
}`}
>
<Image
src={img}
alt={`Thumbnail ${i + 1}`}
fill
sizes="25vw"
style={{ objectFit: 'cover' }}
/>
</button>
))}
</div>
</div>
)
}
Mejores prácticas
1. Siempre usa Image en vez de img
// ❌ Nunca
<img src="/producto.jpg" alt="Producto" />
// ✅ Siempre
<Image src="/producto.jpg" alt="Producto" width={500} height={300} />
2. Define alt descriptivos
// ❌ Mal
<Image src="/producto.jpg" alt="imagen" />
// ✅ Bien
<Image src="/producto.jpg" alt="Camiseta azul de algodón talla M" />
3. Usa priority solo cuando necesario
// ✅ Hero visible inmediatamente
<Image src="/hero.jpg" priority />
// ❌ Imagen que está abajo en la página
<Image src="/footer-logo.jpg" priority /> // No necesario
4. Organiza tus imágenes
public/
├── images/
│ ├── productos/
│ │ ├── camisa-1.jpg
│ │ └── camisa-2.jpg
│ ├── hero/
│ │ └── home-hero.jpg
│ └── logos/
│ └── brand-logo.png
5. Comprime antes de subir
- JPEG: Fotos, imágenes complejas
- PNG: Logos, iconos, transparencias
- SVG: Iconos, ilustraciones simples
- WebP: Alternativa moderna (mejor que JPEG/PNG)
Errores comunes
Error: No width/height
// ❌ Error: Falta width y height
<Image src="/producto.jpg" alt="Producto" />
// ✅ Solución 1: Especificar dimensiones
<Image src="/producto.jpg" alt="Producto" width={500} height={300} />
// ✅ Solución 2: Usar fill
<div className="relative aspect-square">
<Image src="/producto.jpg" alt="Producto" fill />
</div>
Error: Dominio no autorizado
Error: Invalid src prop on `next/image`, hostname "example.com" is not configured
// Solución: next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
}
Error: Layout shift
// ❌ Causa layout shift
<Image src="/img.jpg" alt="Imagen" />
// ✅ Previene layout shift
<Image src="/img.jpg" alt="Imagen" width={500} height={300} />
Resumen
Componente Image de NextJS:
- Optimización automática de tamaño
- Conversión a formatos modernos (WebP/AVIF)
- Lazy loading por defecto
- Previene Layout Shift
- Responsive images automático
Propiedades clave:
width
yheight
para dimensiones fijasfill
para imágenes responsivepriority
para primera imagen visiblesizes
para responsive correctoquality
para controlar compresión
Regla de oro:
Siempre usa Image
en vez de img
. La optimización es automática y dramáticamente mejora la performance de tu sitio.