Next.js Font Optimization — next/font for Zero Layout Shift

Sanjeev SharmaSanjeev Sharma
6 min read

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/font in a Client Component — fonts must be initialized in Server Components or layouts

Best Practices

  • Initialize fonts in app/layout.tsx and pass CSS variables through className on html
  • Load only the exact weights and subsets your design uses
  • Use variable mode with Tailwind CSS for design-system-consistent font application
  • Prefer woff2 format for local fonts — smallest file size with universal modern browser support
  • Disable preload for fonts only visible in below-the-fold sections (footer, modals)
  • Use localFont for premium or custom typefaces that should not be fetched from Google

Key Takeaways

  • next/font self-hosts Google Fonts at build time — no external network request at runtime
  • It generates size-adjust CSS to match fallback fonts, preventing CLS during font swap
  • Use variable mode 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/local supports 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading