Tema
Imágenes y fuentes
Las imágenes y las fuentes suelen ser lo que más pesa en un sitio y lo que más afecta su velocidad. Next.js trae dos componentes que las optimizan automáticamente: next/image (redimensiona, convierte a formatos modernos y carga de forma diferida) y next/font (fuentes sin saltos de diseño ni peticiones a Google). Aprenderás a usarlos bien.
Concepto
Problemas típicos de un sitio sin optimizar:
- Una foto de 4000 px y 5 MB que se muestra a 400 px en un celular.
- Imágenes que "empujan" el contenido al cargar (layout shift, CLS).
- Fuentes que aparecen tarde o cambian el texto de golpe.
next/image y next/font resuelven esto sin configuración extra, y mejoran directamente las Core Web Vitals que Google usa para posicionar.
next/image
tsx
import Image from 'next/image'
export default function Hero() {
return (
<Image
src="/hero.jpg" // archivo en /public
alt="Barista preparando café en una cafetería de Oaxaca"
width={1200} // dimensiones originales (para calcular la proporción)
height={600}
priority // imagen principal visible al cargar: no la difieras
className="h-auto w-full rounded-2xl"
/>
)
}Qué hace por ti:
| Optimización | Detalle |
|---|---|
| Tamaños adaptados | Genera versiones de distintos anchos y el navegador descarga la adecuada |
| Formatos modernos | Sirve WebP o AVIF si el navegador lo soporta |
| Carga diferida | Las imágenes fuera de pantalla se cargan al acercarse (loading="lazy") |
| Sin saltos | Reserva el espacio gracias a width/height |
| Caché | Guarda las versiones optimizadas |
Imágenes importadas (estáticas)
Si importas la imagen, Next conoce sus dimensiones automáticamente y puede generar un desenfoque de carga:
tsx
import Image from 'next/image'
import equipo from '@/public/equipo.jpg' // o './equipo.jpg' junto al componente
export default function Equipo() {
return (
<Image
src={equipo}
alt="Nuestro equipo en la oficina"
placeholder="blur" // muestra una versión borrosa mientras carga
className="rounded-xl"
/>
)
}Imágenes que llenan su contenedor: fill
Cuando no conoces las dimensiones o quieres que la imagen cubra un área (tarjetas, banners):
tsx
import Image from 'next/image'
export function TarjetaProducto({ nombre, imagen }: { nombre: string; imagen: string }) {
return (
<article className="overflow-hidden rounded-xl border">
{/* El padre debe tener position relative y un tamaño definido */}
<div className="relative aspect-square">
<Image
src={imagen}
alt={nombre}
fill
// Indica qué ancho ocupará según la pantalla para descargar el tamaño justo
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-cover transition hover:scale-105"
/>
</div>
<h3 className="p-3 font-medium">{nombre}</h3>
</article>
)
}La prop sizes es importante
Con fill o imágenes responsivas, sin sizes el navegador asume que la imagen ocupa el 100 % del ancho de la pantalla y descarga una versión demasiado grande. Indica cuánto ocupa realmente en cada tamaño de pantalla.
Imágenes remotas
Por seguridad, debes autorizar los dominios externos en next.config.ts:
ts
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.dummyjson.com' },
{ protocol: 'https', hostname: 'res.cloudinary.com', pathname: '/mi-cuenta/**' },
{ protocol: 'https', hostname: '**.amazonaws.com' }
],
formats: ['image/avif', 'image/webp']
}
}
export default nextConfigtsx
<Image
src="https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/thumbnail.webp"
alt="Máscara de pestañas"
width={300}
height={300}
/>Galería de productos
tsx
// app/galeria/page.tsx
import Image from 'next/image'
type Producto = { id: number; title: string; thumbnail: string }
export default async function Galeria() {
const res = await fetch('https://dummyjson.com/products?limit=12&select=title,thumbnail', {
next: { revalidate: 3600 }
})
const { products }: { products: Producto[] } = await res.json()
return (
<main className="mx-auto max-w-6xl p-8">
<h1 className="mb-6 text-3xl font-bold">Galería</h1>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{products.map((p, i) => (
<figure key={p.id} className="overflow-hidden rounded-xl bg-gray-100">
<div className="relative aspect-square">
<Image
src={p.thumbnail}
alt={p.title}
fill
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-contain p-4"
priority={i < 4} // las primeras, visibles sin scroll, se cargan con prioridad
/>
</div>
<figcaption className="p-2 text-center text-sm">{p.title}</figcaption>
</figure>
))}
</div>
</main>
)
}Hosting estático
La optimización de imágenes necesita un servidor (Vercel o next start). Si exportas el sitio como estático para un hosting compartido, usa images: { unoptimized: true } y optimiza tus imágenes antes de subirlas (lo verás en Despliegue).
Buenas prácticas de imágenes
altdescriptivo para accesibilidad y SEO;alt=""sólo para imágenes decorativas.priority(opreloaden Next.js 16) sólo en la imagen principal visible al cargar (normalmente una por página).- Sube imágenes de un tamaño razonable (máximo ~2000 px de ancho); Next redimensiona, pero procesar imágenes gigantes es lento.
- Usa SVG para logos e íconos (con una etiqueta
imgnormal o como componente).
next/font
next/font descarga las fuentes en el build y las sirve desde tu propio dominio:
- Sin peticiones a Google Fonts desde el navegador del usuario (más rápido y más privado).
- Evita el salto de texto al cargar la fuente (ajusta automáticamente la fuente de respaldo).
Fuentes de Google
tsx
// app/layout.tsx
import { Inter, Playfair_Display } from 'next/font/google'
import './globals.css'
// Fuente variable: incluye todos los grosores en un solo archivo
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter' // la exponemos como variable CSS
})
const playfair = Playfair_Display({
subsets: ['latin'],
weight: ['600', '800'], // sólo los grosores que usarás
variable: '--font-playfair'
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={`${inter.variable} ${playfair.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}Conéctalas con Tailwind v4 en globals.css:
css
/* app/globals.css */
@import "tailwindcss";
@theme inline {
--font-sans: var(--font-inter), system-ui, sans-serif;
--font-display: var(--font-playfair), Georgia, serif;
}tsx
<h1 className="font-display text-5xl font-extrabold">Café con historia</h1>
<p className="font-sans">Texto del cuerpo con Inter.</p>Sin Tailwind, también puedes usar className directamente:
tsx
<h1 className={playfair.className}>Título con Playfair</h1>Fuentes locales
Para fuentes de pago o de la marca del cliente:
tsx
// app/fonts.ts
import localFont from 'next/font/local'
export const marca = localFont({
src: [
{ path: './fonts/Marca-Regular.woff2', weight: '400', style: 'normal' },
{ path: './fonts/Marca-Bold.woff2', weight: '700', style: 'normal' }
],
variable: '--font-marca',
display: 'swap'
})tsx
// app/layout.tsx
import { marca } from './fonts'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={marca.variable}>
<body>{children}</body>
</html>
)
}Buena práctica
- Usa máximo 2 familias tipográficas (una para títulos, otra para texto).
- Prefiere fuentes variables y limita los grosores y subconjuntos (
subsets: ['latin']). - Define las fuentes una sola vez (en el layout o en
app/fonts.ts) y reutilízalas.
Error común
Importar Google Fonts con un link en el head o con @import url(...) en el CSS. Pierdes las optimizaciones y agregas peticiones externas que bloquean el render. Usa next/font.
Videos y otros recursos
- Videos: no los sirvas desde tu servidor; usa YouTube/Vimeo (cargados de forma diferida) o un servicio como Mux o Cloudinary. Para un video de fondo corto, usa la etiqueta
videoconmuted,playsInline,preload="none"y unposter. - Scripts de terceros (Google Analytics, píxeles, chat): usa
next/scriptcon la estrategia adecuada para no bloquear la carga:
tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body>
{children}
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX" strategy="afterInteractive" />
<Script id="ga" strategy="afterInteractive">
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXX');`}
</Script>
</body>
</html>
)
}(Para Google Analytics y Tag Manager también existe el paquete oficial @next/third-parties.)
Ejercicios
- Crea una sección hero con una imagen de fondo a pantalla completa usando
fill,priority,object-covery un texto encima con buen contraste. - Configura dos fuentes (una serif para títulos y una sans para el cuerpo) con
next/font/googley úsalas con clases de Tailwind.
Ver solución
tsx
// 1. components/Hero.tsx
import Image from 'next/image'
import Link from 'next/link'
export default function Hero() {
return (
<section className="relative grid min-h-[70vh] place-items-center overflow-hidden text-white">
<Image
src="/hero-cafe.jpg"
alt=""
fill
priority
sizes="100vw"
className="-z-10 object-cover"
/>
{/* Capa oscura para que el texto sea legible */}
<div className="absolute inset-0 -z-10 bg-black/50" />
<div className="max-w-2xl px-6 text-center">
<h1 className="text-4xl font-extrabold sm:text-6xl">Café mexicano de especialidad</h1>
<p className="mt-4 text-lg text-gray-200">Directo de los productores a tu taza.</p>
<Link href="/tienda" className="mt-8 inline-block rounded-full bg-white px-8 py-3 font-semibold text-gray-900">
Comprar ahora
</Link>
</div>
</section>
)
}tsx
// 2. app/layout.tsx
import { Lora, Inter } from 'next/font/google'
import './globals.css'
const lora = Lora({ subsets: ['latin'], variable: '--font-lora', display: 'swap' })
const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap' })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" className={`${lora.variable} ${inter.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}css
/* app/globals.css */
@import "tailwindcss";
@theme inline {
--font-sans: var(--font-inter), system-ui, sans-serif;
--font-serif: var(--font-lora), Georgia, serif;
}tsx
<h1 className="font-serif text-5xl">Título elegante</h1>
<p>Cuerpo en Inter (font-sans por defecto)</p>Resumen
next/imageredimensiona, convierte a WebP/AVIF, difiere la carga y evita saltos de diseño.- Usa
width/heightofill+sizes;prioritysólo en la imagen principal; autoriza dominios remotos ennext.config.ts. next/fontsirve fuentes de Google o locales desde tu dominio, sin saltos de texto.- Carga scripts de terceros con
next/scriptpara no bloquear la página.