Optimización de Fonts

Las fuentes (tipografías) mal cargadas causan:

  • FOIT (Flash of Invisible Text): Texto invisible mientras carga la fuente
  • FOUT (Flash of Unstyled Text): Texto con fuente del sistema y luego cambia
  • Layout Shift: La página "salta" cuando cambia de fuente

NextJS resuelve esto con next/font.

El problema tradicional

html
<!-- Forma tradicional -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
css
body {
  font-family: 'Inter', sans-serif;
}

Problemas:

  • Request extra a Google Fonts (latencia)
  • No hay control sobre el caching
  • FOUT/FOIT visible
  • Privacy concerns (Google ve tus usuarios)

La solución: next/font

NextJS descarga las fuentes en build time y las sirve desde tu propio dominio.

Ventajas:

  • Zero layout shift
  • No requests externos (más rápido)
  • Mejor privacy
  • Self-hosting automático
  • Preloading automático

Google Fonts

Uso básico

tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
 
const inter = Inter({ subsets: ['latin'] })
 
export default function RootLayout({ children }) {
  return (
    <html lang="es" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Eso es todo. NextJS:

  1. Descarga Inter en build time
  2. La sirve desde tu dominio
  3. Preload automático
  4. Zero layout shift

Múltiples pesos

tsx
import { Inter } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin'],
  weight: ['400', '500', '700'],  // Regular, Medium, Bold
})

Rango de pesos (Variable Fonts)

tsx
import { Inter } from 'next/font/google'
 
// Variable font: todos los pesos de 100 a 900
const inter = Inter({
  subsets: ['latin'],
  weight: ['variable'],  // O simplemente omite weight
})

Variable fonts son mejores: un solo archivo con todos los pesos.

Múltiples fuentes

tsx
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
 
// Fuente principal
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})
 
// Fuente para código
const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  variable: '--font-roboto-mono',
})
 
export default function RootLayout({ children }) {
  return (
    <html 
      lang="es" 
      className={`${inter.variable} ${robotoMono.variable}`}
    >
      <body className={inter.className}>{children}</body>
    </html>
  )
}
css
/* app/globals.css */
body {
  font-family: var(--font-inter);
}
 
code, pre {
  font-family: var(--font-roboto-mono);
}

Fuentes locales

Para fuentes custom que tienes en tu proyecto:

Archivo único

tsx
// app/layout.tsx
import localFont from 'next/font/local'
 
const miFont = localFont({
  src: './fonts/MiFuente.woff2',
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="es" className={miFont.className}>
      <body>{children}</body>
    </html>
  )
}

Múltiples archivos (diferentes pesos)

tsx
import localFont from 'next/font/local'
 
const miFont = localFont({
  src: [
    {
      path: './fonts/MiFuente-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './fonts/MiFuente-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
    {
      path: './fonts/MiFuente-Italic.woff2',
      weight: '400',
      style: 'italic',
    },
  ],
})

Variable local font

tsx
import localFont from 'next/font/local'
 
const miFont = localFont({
  src: './fonts/MiFuente-Variable.woff2',
  variable: '--font-custom',
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="es" className={miFont.variable}>
      <body>{children}</body>
    </html>
  )
}

Ejemplos prácticos

Setup completo con Tailwind

tsx
// app/layout.tsx
import { Inter, Poppins } from 'next/font/google'
import './globals.css'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
})
 
const poppins = Poppins({
  subsets: ['latin'],
  weight: ['400', '600', '700'],
  variable: '--font-poppins',
  display: 'swap',
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="es" className={`${inter.variable} ${poppins.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  )
}
js
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        heading: ['var(--font-poppins)'],
      },
    },
  },
}
tsx
// Usar en componentes
<h1 className="font-heading text-4xl font-bold">Título</h1>
<p className="font-sans">Párrafo con Inter</p>

Fuente solo en una página

tsx
// app/blog/layout.tsx
import { Merriweather } from 'next/font/google'
 
const merriweather = Merriweather({
  subsets: ['latin'],
  weight: ['400', '700'],
})
 
export default function BlogLayout({ children }) {
  return <div className={merriweather.className}>{children}</div>
}

Solo las páginas bajo /blog usarán Merriweather.

Fuente en componente específico

tsx
// components/Logo.tsx
import { Pacifico } from 'next/font/google'
 
const pacifico = Pacifico({
  subsets: ['latin'],
  weight: '400',
})
 
export default function Logo() {
  return (
    <h1 className={pacifico.className}>
      MiTienda
    </h1>
  )
}

Fuente con fallback personalizado

tsx
import { Inter } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin'],
  fallback: ['system-ui', 'arial'],  // Si falla, usa estas
})

Propiedades de configuración

subsets

Qué caracteres incluir:

tsx
// Solo caracteres latinos
{ subsets: ['latin'] }
 
// Latino + latino extendido (ñ, á, etc)
{ subsets: ['latin', 'latin-ext'] }
 
// Múltiples idiomas
{ subsets: ['latin', 'cyrillic', 'greek'] }

Tip: Usa solo los subsets que necesitas para reducir tamaño.

display

Controla el comportamiento mientras carga:

tsx
import { Inter } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // Recomendado
})

Opciones:

  • 'swap': Muestra fallback inmediatamente, cambia cuando carga (recomendado)
  • 'optional': Usa fuente custom solo si carga rápido, sino usa fallback
  • 'block': Invisible max 3s mientras carga (no recomendado)
  • 'fallback': Invisible muy poco tiempo, luego muestra fallback

Recomendación: Usa 'swap' siempre.

preload

tsx
const inter = Inter({
  subsets: ['latin'],
  preload: true,  // Default: true
})

Siempre déjalo en true para la fuente principal.

variable

Para CSS variables:

tsx
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})
 
// Usar con Tailwind
<p className="font-sans">  {/* usa var(--font-inter) */}

adjustFontFallback

Para prevenir layout shift:

tsx
const inter = Inter({
  subsets: ['latin'],
  adjustFontFallback: true,  // Default: true para Google Fonts
})

NextJS ajusta métricas del fallback para que coincida con la fuente custom.

Fuentes populares

Para interfaces (UI)

tsx
// Inter - La más popular para UI
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
 
// Roboto - Material Design
import { Roboto } from 'next/font/google'
const roboto = Roboto({ 
  subsets: ['latin'],
  weight: ['400', '500', '700']
})
 
// Open Sans - Versátil
import { Open_Sans } from 'next/font/google'
const openSans = Open_Sans({ subsets: ['latin'] })
 
// Poppins - Moderna y amigable
import { Poppins } from 'next/font/google'
const poppins = Poppins({
  subsets: ['latin'],
  weight: ['400', '600', '700']
})

Para títulos

tsx
// Playfair Display - Elegante
import { Playfair_Display } from 'next/font/google'
const playfair = Playfair_Display({ subsets: ['latin'] })
 
// Montserrat - Geométrica
import { Montserrat } from 'next/font/google'
const montserrat = Montserrat({ subsets: ['latin'] })
 
// Bebas Neue - Bold y condensada
import { Bebas_Neue } from 'next/font/google'
const bebasNeue = Bebas_Neue({ 
  subsets: ['latin'],
  weight: '400'
})

Para código

tsx
// Fira Code - Con ligaduras
import { Fira_Code } from 'next/font/google'
const firaCode = Fira_Code({ subsets: ['latin'] })
 
// JetBrains Mono - Diseñada para código
import { JetBrains_Mono } from 'next/font/google'
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'] })
 
// Source Code Pro - Adobe
import { Source_Code_Pro } from 'next/font/google'
const sourceCodePro = Source_Code_Pro({ subsets: ['latin'] })

Para lectura (blogs)

tsx
// Merriweather - Serif para lectura larga
import { Merriweather } from 'next/font/google'
const merriweather = Merriweather({
  subsets: ['latin'],
  weight: ['400', '700']
})
 
// Lora - Elegante para blogs
import { Lora } from 'next/font/google'
const lora = Lora({ subsets: ['latin'] })
 
// PT Serif - Clásica
import { PT_Serif } from 'next/font/google'
const ptSerif = PT_Serif({
  subsets: ['latin'],
  weight: ['400', '700']
})

Patrones de uso

Sistema de fuentes

tsx
// lib/fonts.ts
import { Inter, Poppins, Fira_Code } from 'next/font/google'
 
export const fontSans = Inter({
  subsets: ['latin'],
  variable: '--font-sans',
  display: 'swap',
})
 
export const fontHeading = Poppins({
  subsets: ['latin'],
  weight: ['600', '700'],
  variable: '--font-heading',
  display: 'swap',
})
 
export const fontMono = Fira_Code({
  subsets: ['latin'],
  variable: '--font-mono',
  display: 'swap',
})
tsx
// app/layout.tsx
import { fontSans, fontHeading, fontMono } from '@/lib/fonts'
 
export default function RootLayout({ children }) {
  return (
    <html 
      lang="es" 
      className={`${fontSans.variable} ${fontHeading.variable} ${fontMono.variable}`}
    >
      <body className={fontSans.className}>{children}</body>
    </html>
  )
}
css
/* globals.css */
body {
  font-family: var(--font-sans);
}
 
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
}
 
code, pre {
  font-family: var(--font-mono);
}

Fuente según el idioma

tsx
import { Inter, Noto_Sans_JP } from 'next/font/google'
 
const inter = Inter({ subsets: ['latin'] })
const notoSansJP = Noto_Sans_JP({ subsets: ['japanese'] })
 
export default function RootLayout({ children, params: { locale } }) {
  const font = locale === 'ja' ? notoSansJP : inter
  
  return (
    <html lang={locale} className={font.className}>
      <body>{children}</body>
    </html>
  )
}

Fuente con modo oscuro

tsx
// Las fuentes funcionan igual en dark mode
// Solo ajusta el weight si es necesario
 
<html className={inter.className}>
  <body className="dark:text-white">
    <h1 className="font-bold dark:font-semibold">
      {/* Tal vez menos peso en dark mode */}
    </h1>
  </body>
</html>

Performance

Preload solo lo necesario

tsx
// Mal: Preload de todas las fuentes
const inter = Inter({ subsets: ['latin'], preload: true })
const roboto = Roboto({ subsets: ['latin'], preload: true })
const poppins = Poppins({ subsets: ['latin'], preload: true })
 
// Bien: Preload solo la principal
const inter = Inter({ subsets: ['latin'], preload: true })
const roboto = Roboto({ subsets: ['latin'], preload: false })

Usa variable fonts cuando sea posible

tsx
// Múltiples archivos
const inter = Inter({
  weight: ['400', '500', '600', '700'],  // 4 archivos
})
 
// Variable font: 1 archivo
const inter = Inter({
  // Sin especificar weight = variable font
})

Limita los subsets

tsx
// Todos los caracteres
{ subsets: ['latin', 'latin-ext', 'cyrillic', 'greek'] }
 
// Solo lo que necesitas
{ subsets: ['latin'] }  // Si tu sitio es solo español/inglés

CSS @font-face generado

NextJS genera automáticamente:

css
@font-face {
  font-family: '__Inter_abc123';
  src: url(/_next/static/media/abc123-Regular.woff2) format('woff2');
  font-display: swap;
  font-weight: 400;
}
 
@font-face {
  font-family: '__Inter_abc123';
  src: url(/_next/static/media/abc123-Bold.woff2) format('woff2');
  font-display: swap;
  font-weight: 700;
}

No necesitas escribir esto, NextJS lo hace automáticamente.

Debugging

Ver fuentes cargadas

tsx
// Componente de debugging
export function FontDebug() {
  return (
    <div className="p-4 space-y-2">
      <p className="font-sans">Sans: The quick brown fox</p>
      <p className="font-heading">Heading: The quick brown fox</p>
      <p className="font-mono">Mono: The quick brown fox</p>
      
      <div className="space-y-1">
        <p className="font-thin">Thin 100</p>
        <p className="font-light">Light 300</p>
        <p className="font-normal">Normal 400</p>
        <p className="font-medium">Medium 500</p>
        <p className="font-semibold">Semibold 600</p>
        <p className="font-bold">Bold 700</p>
        <p className="font-extrabold">Extrabold 800</p>
        <p className="font-black">Black 900</p>
      </div>
    </div>
  )
}

Verificar en DevTools

plaintext
Chrome DevTools → Network → Filter: "font"

Deberías ver las fuentes servidas desde tu dominio, no de Google.

Migrando desde Google Fonts CDN

Antes (CDN)

html
<!-- index.html -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
css
body {
  font-family: 'Inter', sans-serif;
}

Después (next/font)

tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin'],
  weight: ['400', '700'],
})
 
export default function RootLayout({ children }) {
  return (
    <html lang="es" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Elimina el <link> de Google Fonts. NextJS se encarga de todo.

Mejores prácticas

1. Una fuente principal, máximo dos

tsx
// Bien: Una fuente variable
const inter = Inter({ subsets: ['latin'] })
 
// Bien: Dos fuentes (texto + títulos)
const inter = Inter({ subsets: ['latin'] })
const poppins = Poppins({ subsets: ['latin'], weight: ['600', '700'] })
 
// Mal: Muchas fuentes
const font1 = Inter({ subsets: ['latin'] })
const font2 = Roboto({ subsets: ['latin'] })
const font3 = Open_Sans({ subsets: ['latin'] })
const font4 = Montserrat({ subsets: ['latin'] })  // Demasiadas

2. Usa display: 'swap'

tsx
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // Siempre
})

3. Preload solo la fuente principal

tsx
const inter = Inter({ 
  preload: true,  // Fuente principal
})
 
const secondary = Roboto({
  preload: false,  // Fuentes secundarias
})

4. Usa variable fonts

tsx
// Variable font: 1 archivo con todos los pesos
const inter = Inter({ subsets: ['latin'] })
 
// Múltiples pesos: múltiples archivos
const inter = Inter({ 
  subsets: ['latin'],
  weight: ['400', '500', '600', '700', '800']
})

5. Coloca fuentes en layout.tsx

tsx
// Bien: En layout
// app/layout.tsx
const inter = Inter({ subsets: ['latin'] })
 
// Mal: En cada página
// app/page.tsx
const inter = Inter({ subsets: ['latin'] })  // Se vuelve a cargar

Errores comunes

Error: Fuente no encontrada

tsx
// Nombre incorrecto
import { Inter_Bold } from 'next/font/google'
 
// Correcto
import { Inter } from 'next/font/google'
const inter = Inter({ weight: '700' })

Error: Formato de archivo local

tsx
// NextJS no soporta .ttf directamente
src: './fonts/MiFuente.ttf'
 
// Usa .woff2 (más comprimido)
src: './fonts/MiFuente.woff2'

Convierte .ttf a .woff2 con Transfonter.

Fuente no se aplica

tsx
// Olvidaste aplicar la clase
<html lang="es">
 
// Aplica la clase
<html lang="es" className={inter.className}>

Recursos

  • Google Fonts: fonts.google.com
  • Font Squirrel: Fuentes gratuitas para descargar
  • Transfonter: Convertir fuentes a .woff2
  • FontPair: Combinaciones de fuentes
  • Type Scale: Generar escalas de tamaño

Resumen

next/font en NextJS:

  • Self-hosting automático de Google Fonts
  • Zero layout shift
  • Mejor privacy y performance
  • Preloading automático
  • Soporte para fuentes locales

Setup básico:

  1. Importa la fuente de next/font/google
  2. Configura con subsets y display: 'swap'
  3. Aplica className en tu layout
  4. ¡Listo!

Regla de oro: Usa una fuente variable para texto y opcionalmente otra para títulos. Menos fuentes = sitio más rápido.