Next.js

Font Optimization in Next.js

16 min Lesson 14 of 40

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