Skip to content

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ónDetalle
Tamaños adaptadosGenera versiones de distintos anchos y el navegador descarga la adecuada
Formatos modernosSirve WebP o AVIF si el navegador lo soporta
Carga diferidaLas imágenes fuera de pantalla se cargan al acercarse (loading="lazy")
Sin saltosReserva 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 nextConfig
tsx
<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 ​

  • alt descriptivo para accesibilidad y SEO; alt="" sólo para imágenes decorativas.
  • priority (o preload en 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 img normal 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 video con muted, playsInline, preload="none" y un poster.
  • Scripts de terceros (Google Analytics, píxeles, chat): usa next/script con 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 ​

  1. Crea una sección hero con una imagen de fondo a pantalla completa usando fill, priority, object-cover y un texto encima con buen contraste.
  2. Configura dos fuentes (una serif para títulos y una sans para el cuerpo) con next/font/google y ú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/image redimensiona, convierte a WebP/AVIF, difiere la carga y evita saltos de diseño.
  • Usa width/height o fill + sizes; priority sólo en la imagen principal; autoriza dominios remotos en next.config.ts.
  • next/font sirve fuentes de Google o locales desde tu dominio, sin saltos de texto.
  • Carga scripts de terceros con next/script para no bloquear la página.

Academia Web — aprende, copia y construye.