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

<!-- ❌ 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">
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

// 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

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  weight: ['400', '500', '700'],  // Regular, Medium, Bold
})

Rango de pesos (Variable Fonts)

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

// 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>
  )
}
/* 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

// 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)

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

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

// 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>
  )
}
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        heading: ['var(--font-poppins)'],
      },
    },
  },
}
// 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

// 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

// 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

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:

// 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:

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

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

Siempre déjalo en true para la fuente principal.

variable

Para CSS variables:

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})

// Usar con Tailwind
<p className="font-sans">  {/* usa var(--font-inter) */}

adjustFontFallback

Para prevenir layout shift:

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)

// 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

// 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

// 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)

// 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

// 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',
})
// 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>
  )
}
/* 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

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

// 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

// ❌ 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

// ❌ 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

// ❌ 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:

@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

// 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

Chrome DevTools → Network → Filter: "font"

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

Migrando desde Google Fonts CDN

Antes (CDN)

<!-- 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">
body {
  font-family: 'Inter', sans-serif;
}

Después (next/font)

// 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

// ✅ 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'

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

3. Preload solo la fuente principal

const inter = Inter({ 
  preload: true,  // Fuente principal
})

const secondary = Roboto({
  preload: false,  // Fuentes secundarias
})

4. Usa variable fonts

// ✅ 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

// ✅ 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

// ❌ 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

// ❌ 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

// ❌ 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.