Next.js Font Optimization — next/font for Zero Layout Shift
Advertisement
Introduction
next/font is a built-in Next.js module that eliminates Cumulative Layout Shift (CLS) caused by web fonts and removes external network requests to Google Fonts. It automatically self-hosts fonts at build time, generates CSS size-adjust to match fallback fonts, and ensures fonts load without flashing unstyled text (FOUT). The result: pixel-perfect typography with no performance penalty.
Why This Matters
Web fonts are a hidden performance killer. Loading fonts from Google Fonts requires a DNS lookup, TCP connection, TLS handshake, and HTTP request — all before your text renders. This causes the browser to either show invisible text (FOIT) or flash your fallback font then swap (FOUT), both contributing to poor CLS scores.
next/font solves this by downloading the font files at build time and serving them from your own domain as part of the Next.js static asset pipeline. Combined with size-adjust and ascent-override CSS properties that make the fallback font match the loaded font's metrics, CLS from font swapping is effectively eliminated.
In 2025, CLS below 0.1 is Google's "good" threshold and affects search rankings. Font optimization with next/font is one of the fastest ways to fix CLS issues.
Loading Google Fonts
// app/layout.tsx
import { Inter, Playfair_Display } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const playfair = Playfair_Display({
subsets: ['latin'],
weight: ['400', '700'],
style: ['normal', 'italic'],
display: 'swap',
variable: '--font-playfair',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${playfair.variable}`}>
<body className={inter.className}>{children}</body>
</html>
)
}Using Font Variables with Tailwind CSS
Define CSS variables and configure Tailwind to use them:
// app/layout.tsx
import { Inter, Merriweather } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], variable: '--font-sans' })
const merriweather = Merriweather({
subsets: ['latin'],
weight: ['300', '400', '700'],
variable: '--font-serif',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${merriweather.variable}`}>
<body>{children}</body>
</html>
)
}// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
serif: ['var(--font-serif)', 'Georgia', 'serif'],
},
},
},
}Now use font-sans and font-serif Tailwind classes anywhere in your app.
Loading a Single Font Weight
For optimal performance, only load the font weights you actually use:
import { Roboto } from 'next/font/google'
const roboto = Roboto({
weight: '400',
subsets: ['latin'],
display: 'swap',
})
export default function Page() {
return (
<main className={roboto.className}>
<h1>Hello World</h1>
</main>
)
}Each additional weight adds ~20–40KB. Only include weights that appear in your design system.
Local Custom Fonts
Use next/font/local for custom font files stored in your project:
// app/layout.tsx
import localFont from 'next/font/local'
const geist = localFont({
src: [
{
path: '../public/fonts/Geist-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: '../public/fonts/Geist-Bold.woff2',
weight: '700',
style: 'normal',
},
{
path: '../public/fonts/Geist-Italic.woff2',
weight: '400',
style: 'italic',
},
],
variable: '--font-geist',
display: 'swap',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={geist.variable}>
<body className={geist.className}>{children}</body>
</html>
)
}Font Subsets for Internationalization
Load only the character subsets you need. Loading all subsets unnecessarily adds hundreds of KB:
import { Noto_Sans } from 'next/font/google'
// Japanese support
const notoSans = Noto_Sans({
subsets: ['latin', 'japanese'],
weight: ['400', '700'],
display: 'swap',
})
// Cyrillic for Russian/Eastern European
const inter = Inter({
subsets: ['latin', 'cyrillic'],
display: 'swap',
})Available subsets vary by font. Check the Google Fonts page for each font's supported subsets.
Preloading for Critical Fonts
Set preload: true (the default) to add a <link rel="preload"> for the font. Disable it for fonts that are not immediately visible:
const bodyFont = Inter({
subsets: ['latin'],
preload: true, // default — adds <link rel="preload">
})
const footerFont = Roboto_Mono({
subsets: ['latin'],
preload: false, // Don't preload fonts only used in footer
weight: '400',
})fallback Fonts for CLS Prevention
Specify fallback fonts that match the loaded font's metrics:
const inter = Inter({
subsets: ['latin'],
display: 'swap',
fallback: ['system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'],
})Next.js uses size-adjust, ascent-override, and descent-override CSS properties on the fallback font to make it visually match the loaded font, eliminating layout shift during the font swap.
Per-Page Font Loading
Load different fonts for specific pages without affecting the global layout:
// app/blog/layout.tsx
import { Lora } from 'next/font/google'
const lora = Lora({
subsets: ['latin'],
display: 'swap',
})
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div className={lora.className}>
{children}
</div>
)
}Common Mistakes
- Importing a font inside a component instead of a layout — creates multiple font instances
- Loading all font weights (100–900) when only 400 and 700 are used — wastes 100–200KB
- Not setting
display: 'swap'— text stays invisible until the font loads (FOIT) - Using
@import url('https://fonts.googleapis.com/...')in CSS — bypasses all Next.js optimizations - Loading
next/fontin a Client Component — fonts must be initialized in Server Components or layouts
Best Practices
- Initialize fonts in
app/layout.tsxand pass CSS variables throughclassNameonhtml - Load only the exact weights and subsets your design uses
- Use
variablemode with Tailwind CSS for design-system-consistent font application - Prefer
woff2format for local fonts — smallest file size with universal modern browser support - Disable
preloadfor fonts only visible in below-the-fold sections (footer, modals) - Use
localFontfor premium or custom typefaces that should not be fetched from Google
Key Takeaways
next/fontself-hosts Google Fonts at build time — no external network request at runtime- It generates
size-adjustCSS to match fallback fonts, preventing CLS during font swap - Use
variablemode to expose font as a CSS custom property — works seamlessly with Tailwind display: 'swap'shows text in fallback font immediately, swaps when custom font loads- Only load the font weights and subsets you actually use — each weight adds 20–40KB
next/font/localsupports custom font files with multiple weights and styles in one call- Fonts must be initialized in Server Components or layouts — not inside Client Components
- Preloading is enabled by default — disable it only for fonts not visible on initial page load
Advertisement