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:
- Descarga Inter en build time
- La sirve desde tu dominio
- Preload automático
- 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:
- Importa la fuente de
next/font/google
- Configura con
subsets
ydisplay: 'swap'
- Aplica
className
en tu layout - ¡Listo!
Regla de oro: Usa una fuente variable para texto y opcionalmente otra para títulos. Menos fuentes = sitio más rápido.