Introduction to Font Optimization
Web fonts significantly impact page load performance and user experience. Loading external fonts from services like Google Fonts can cause layout shifts and slow down your site. Next.js provides the next/font module that automatically optimizes fonts by self-hosting them, eliminating external network requests, and preventing layout shifts.
Note: next/font automatically downloads font files at build time and self-hosts them with your static assets. This means zero layout shift, no external requests, and better privacy for your users.
Using Google Fonts
Next.js makes it incredibly easy to use Google Fonts with automatic optimization:
Basic Google Font Usage
// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
This code:
- Imports the Inter font from Google Fonts
- Downloads it at build time
- Self-hosts it with your application
- Applies it to all pages via the root layout
Specifying Font Weights
import { Roboto } from 'next/font/google';
const roboto = Roboto({
weight: ['400', '700'],
subsets: ['latin'],
display: 'swap',
});
Single Weight Font
import { Playfair_Display } from 'next/font/google';
const playfair = Playfair_Display({
weight: '400',
subsets: ['latin'],
});
Variable Fonts
Variable fonts provide multiple styles in a single file:
import { Inter } from 'next/font/google';
// Variable fonts don't need weight specification
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
);
}
// globals.css
body {
font-family: var(--font-inter);
}
h1 {
font-family: var(--font-inter);
font-weight: 700;
}
Tip: Variable fonts are more efficient than loading multiple font weights separately. A single variable font file can contain all weights from 100 to 900, reducing total file size and HTTP requests.
Using Multiple Fonts
Combine multiple fonts for headings, body text, and special elements:
// app/layout.tsx
import { Inter, Playfair_Display } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
const playfair = Playfair_Display({
subsets: ['latin'],
variable: '--font-playfair',
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${playfair.variable}`}>
<body>{children}</body>
</html>
);
}
// globals.css
body {
font-family: var(--font-inter), sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-playfair), serif;
}
Font Subsets
Optimize loading by specifying which character subsets you need:
import { Noto_Sans } from 'next/font/google';
const notoSans = Noto_Sans({
weight: ['400', '700'],
subsets: ['latin', 'latin-ext'], // Latin + Extended Latin
});
Available Subsets (vary by font)
- latin (Western European languages)
- latin-ext (Extended Latin characters)
- cyrillic (Russian, Ukrainian, etc.)
- greek (Greek language)
- arabic (Arabic languages)
- hebrew (Hebrew language)
- vietnamese (Vietnamese language)
- And many more...
Arabic Font Example
import { Cairo } from 'next/font/google';
const cairo = Cairo({
weight: ['400', '700'],
subsets: ['arabic', 'latin'],
variable: '--font-cairo',
});
Warning: Only include the subsets you actually need. Including unnecessary subsets increases font file size and loading time. For an English-only site, only use the 'latin' subset.
Local Fonts
Use custom fonts stored in your project:
Basic Local Font
// app/layout.tsx
import localFont from 'next/font/local';
const myFont = localFont({
src: './fonts/MyCustomFont.woff2',
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
);
}
Multiple Font Files (Different Weights)
import localFont from 'next/font/local';
const myFont = localFont({
src: [
{
path: './fonts/MyFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/MyFont-Italic.woff2',
weight: '400',
style: 'italic',
},
{
path: './fonts/MyFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-my-custom',
});
Variable Local Font
import localFont from 'next/font/local';
const myVariableFont = localFont({
src: './fonts/MyVariableFont.woff2',
variable: '--font-my-variable',
weight: '100 900', // Weight range for variable font
});
Font Display Strategies
Control how fonts are displayed during loading:
Display Options
// swap (recommended): Show fallback, then swap to custom font
const font = Inter({ display: 'swap' });
// optional: Show fallback for 100ms, then swap or keep fallback
const font = Inter({ display: 'optional' });
// block: Hide text for up to 3s, then show custom font
const font = Inter({ display: 'block' });
// fallback: Show fallback for 100ms, then swap with 3s timeout
const font = Inter({ display: 'fallback' });
// auto: Browser decides
const font = Inter({ display: 'auto' });
Recommended: Use display: 'swap' for most cases. It shows text immediately with a fallback font, then seamlessly switches to your custom font when loaded. This prevents invisible text and improves perceived performance.
Applying Fonts to Specific Components
Apply fonts to individual components rather than globally:
Component-Level Font
// app/components/Heading.tsx
import { Playfair_Display } from 'next/font/google';
const playfair = Playfair_Display({
weight: '700',
subsets: ['latin'],
});
export default function Heading({ children }) {
return (
<h1 className={playfair.className}>
{children}
</h1>
);
}
Conditional Font Application
// app/page.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
const robotoMono = Roboto_Mono({ subsets: ['latin'] });
export default function Page() {
return (
<div>
<p className={inter.className}>Regular text</p>
<code className={robotoMono.className}>Code snippet</code>
</div>
);
}
Preloading Fonts
Next.js automatically preloads fonts used in your application, but you can control this:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
preload: true, // Default
});
// Disable preloading (not recommended)
const optionalFont = Inter({
subsets: ['latin'],
preload: false,
});
Font Fallbacks
Specify fallback fonts for better layout stability:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
fallback: ['system-ui', 'arial'],
});
Adjusted Fallbacks
Fine-tune fallback metrics to match your custom font:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
adjustFontFallback: true, // Default, auto-adjusts metrics
});
// Or disable
const noAdjust = Inter({
subsets: ['latin'],
adjustFontFallback: false,
});
Complete Font Configuration Example
Production-ready font setup with multiple fonts:
// app/fonts.ts
import { Inter, Playfair_Display, Roboto_Mono } from 'next/font/google';
import localFont from 'next/font/local';
// Body font (variable)
export const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
// Heading font
export const playfair = Playfair_Display({
weight: ['400', '700'],
subsets: ['latin'],
variable: '--font-playfair',
display: 'swap',
});
// Code font
export const robotoMono = Roboto_Mono({
weight: ['400', '700'],
subsets: ['latin'],
variable: '--font-roboto-mono',
display: 'swap',
});
// Custom brand font
export const brandFont = localFont({
src: './fonts/BrandFont-Variable.woff2',
variable: '--font-brand',
display: 'swap',
weight: '100 900',
});
// app/layout.tsx
import { inter, playfair, robotoMono, brandFont } from './fonts';
export default function RootLayout({ children }) {
return (
<html
lang="en"
className={`${inter.variable} ${playfair.variable} ${robotoMono.variable} ${brandFont.variable}`}
>
<body>{children}</body>
</html>
);
}
// app/globals.css
:root {
--font-body: var(--font-inter), system-ui, sans-serif;
--font-heading: var(--font-playfair), Georgia, serif;
--font-code: var(--font-roboto-mono), 'Courier New', monospace;
--font-brand: var(--font-brand), sans-serif;
}
body {
font-family: var(--font-body);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
}
code, pre {
font-family: var(--font-code);
}
.brand-text {
font-family: var(--font-brand);
}
Performance Best Practices
1. Use Variable Fonts When Possible
// Good: One variable font file
const inter = Inter({ subsets: ['latin'] });
// Less optimal: Multiple weight files
const roboto = Roboto({
weight: ['300', '400', '500', '700', '900'],
subsets: ['latin'],
});
2. Limit Font Weights
// Bad: Loading all weights
const tooMany = Roboto({
weight: ['100', '300', '400', '500', '700', '900'],
});
// Good: Only weights you actually use
const minimal = Roboto({
weight: ['400', '700'],
});
3. Use Appropriate Subsets
// Bad: Loading unnecessary subsets
const wasteful = Noto_Sans({
subsets: ['latin', 'cyrillic', 'greek', 'arabic'],
});
// Good: Only needed subset
const efficient = Noto_Sans({
subsets: ['latin'],
});
4. Minimize Number of Fonts
// Optimal: 2-3 font families maximum
import { Inter, Playfair_Display } from 'next/font/google';
// Avoid: Too many different fonts
import { Inter, Roboto, Lato, OpenSans, Montserrat } from 'next/font/google';
Troubleshooting Common Issues
Issue: Font not loading
Solution: Check font name spelling and ensure it's available on Google Fonts
// Wrong: Incorrect name
import { InterFont } from 'next/font/google'; // ❌
// Correct
import { Inter } from 'next/font/google'; // ✅
Issue: Layout shift occurring
Solution: Use font-display: swap and adjustFontFallback
const inter = Inter({
subsets: ['latin'],
display: 'swap',
adjustFontFallback: true,
});
Issue: Fonts not applying
Solution: Ensure className or variable is properly applied
// Check you're using the font
<html className={inter.className}> // For className
<html className={inter.variable}> // For CSS variable
Exercise: Set up a complete font system for a blog with:
- Inter (variable) for body text
- Playfair Display for headings
- Fira Code for code blocks
- A custom local font for your brand logo
Configure proper subsets, display strategies, and CSS variables. Create a sample page demonstrating all fonts with proper fallbacks.
Summary
Next.js font optimization provides automatic performance improvements. Key takeaways:
- Use next/font/google for automatic Google Fonts optimization
- Self-hosting eliminates external requests and improves privacy
- Variable fonts are more efficient than multiple weight files
- Specify only the subsets you need to reduce file size
- Use display: 'swap' for best user experience
- Apply fonts via className or CSS variables
- Local fonts work with localFont for custom typography
- Limit font families to 2-3 for optimal performance
- Next.js automatically preloads fonts to prevent layout shifts