How to Eliminate Font CLS with next/font

This guide is part of Next.js Performance, within Framework Performance. Web fonts cause two kinds of performance problems. Loaded from a third-party stylesheet (such as Google Fonts' CSS API), they add render-blocking CSS on another origin and delay text rendering. When they arrive, they replace the fallback font, and because fallback and web font have different metrics (character widths, ascent, line height), text reflows — a layout shift that counts towards CLS. On text-heavy pages, font swaps are one of the most common sources of CLS.

next/font addresses both. It downloads Google Fonts (or uses local font files) at build time and self-hosts them with the app, so there is no third-party request. It preloads the font files, and it generates a fallback @font-face using size-adjust, ascent-override, descent-override and line-gap-override, so the fallback font occupies nearly the same space as the web font. When the web font loads, text barely moves.

Google Fonts stylesheet vs next/font Comparison of loading fonts from the Google Fonts CSS API with self-hosting them through next/font. Google Fonts stylesheet vs next/font Google Fonts stylesheet • Render-blocking CSS on another origin • Extra DNS, TCP and TLS setup • Fallback metrics differ from web font • Text reflows when font loads (CLS) next/font • Fonts self-hosted with the app • Preloaded, same origin • Size-adjusted fallback font-face • Minimal shift when font swaps

Rapid Diagnosis

  • Check for fonts.googleapis.com links or @import in CSS.
  • Check CLS attribution: shifts of text blocks at the moment fonts load.
  • Record a filmstrip: visible text reflow when the web font appears.
  • Check font file count and size: many weights and styles add download time.

Root Cause Analysis

1. Third-party font CSS. Render-blocking and late font discovery.

2. Metric mismatch. Fallback fonts take different space than web fonts.

3. Too many font files. Every weight and style is a separate download.

4. No preload. Fonts discovered only after CSS is parsed.

Step-by-Step Resolution

1. Load fonts with next/font

javascript
// app/layout.js
import { Inter, Source_Serif_4 } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-sans' });
const serif = Source_Serif_4({ subsets: ['latin'], weight: ['400', '600'], variable: '--font-serif' });
export default function RootLayout({ children }) {
  return (<html lang="en" className={`${inter.variable} ${serif.variable}`}><body>{children}</body></html>);
}
// trade-off: each additional family and weight adds preloaded bytes; prefer one
// variable font for the body text and limit display fonts to what pages use.

2. Use CSS variables in styles

Reference var(--font-sans) and var(--font-serif) in your CSS or Tailwind configuration, so the generated font-face and fallback are used consistently.

3. Use local fonts for custom typefaces

javascript
import localFont from 'next/font/local';
const brand = localFont({ src: [{ path: './fonts/Brand-Var.woff2', weight: '100 900' }], variable: '--font-brand', display: 'swap' });

4. Limit subsets, weights and preloads

Load only the subsets you need (for example latin), prefer variable fonts over multiple static weights, and set preload: false for fonts used only below the fold or on few pages.

CLS from font swap on an article template (p75) Bar chart comparing CLS at the 75th percentile with Google Fonts stylesheet loading and with next/font. CLS from font swap on an article template (p75) Google Fonts CSS, display swap 140CLS x 1000 Self-hosted, no fallback adjust 95CLS x 1000 next/font (adjusted fallback) 12CLS x 1000 CLS good (0.1)

Verification

In DevTools, confirm fonts load from your own origin and are preloaded (initiator: preload). Record the page with a slow connection and check the filmstrip: text should not visibly reflow when the font loads. Check the layout shift entries: no shifts attributed to text blocks at font load. In the field, CLS p75 for text-heavy templates should drop.

Worked Example: A Blog Platform

A blog platform loaded two Google Fonts families (four weights each) through a stylesheet link. CLS p75 on article pages was 0.14, mostly from font swaps in headlines and body text, and the font CSS added 250ms of render-blocking time on mobile. Switching to next/font with a variable body font and one display font weight cut font downloads from eight files to two, removed the third-party connection, and the generated fallbacks reduced font-related shifts to near zero. CLS p75 fell to 0.03 and FCP improved by 280ms.

How the Fallback Adjustment Works

next/font computes metrics for the web font and generates a fallback @font-face that points at a system font (such as Arial or Times New Roman) with size-adjust (scales glyph widths) and ascent/descent/line-gap overrides (vertical metrics) chosen to match the web font. The fallback is listed after the web font in the generated font-family. During loading, text renders in the adjusted fallback, occupying almost exactly the space it will take in the web font. The swap still changes the glyph shapes, but not the layout. For custom local fonts, adjustFontFallback can be configured, and for unusual typefaces, manual tuning may still be needed.

Choosing font-display

display: 'swap' shows fallback text immediately and swaps when the font arrives — good for content, and with adjusted fallbacks, nearly shift-free. optional uses the web font only if it is available almost immediately (cached or very fast), otherwise sticks with the fallback for that page view, eliminating swaps entirely at the cost of sometimes not showing the web font. block hides text briefly while waiting, which can delay LCP for text elements. Most sites should use swap with adjusted fallbacks; optional suits sites that prioritise stability over brand typography on slow first visits.

font-display options with next/font How font-display values behave and when to use them. font-display options with next/font Value Text visible immediately Swap shift risk Use when swap yes low with adjusted fallback most sites optional yes none stability first block briefly hidden low icon fonts (avoid) fallback after short block low compromise

Auditing Fonts Across an App

Font usage grows over time: a marketing page adds a display face, a component library brings its own font, a blog template uses a different weight. Each family and weight adds preloaded bytes or later downloads. Periodically list the fonts loaded on key pages (DevTools Network panel, filtered by Font) and compare with the design system. Consolidate to one variable font for text and at most one display font, define them once in the root layout, and expose them as CSS variables used everywhere. Remove stray @font-face rules and stylesheet links left from earlier implementations, which can quietly reintroduce third-party requests and unadjusted fallbacks.

Common Mistakes

  • Keeping the Google Fonts link alongside next/font. Double downloads.
  • Loading many static weights. Use a variable font.
  • Applying fonts via class names inconsistently. Some text uses the unadjusted family.
  • Preloading fonts used rarely. Wastes bandwidth on every page.

Edge Cases

Icon fonts. Prefer SVG icons; icon fonts render boxes or wrong glyphs before loading.

Multiple scripts (languages). Include the subsets you need; CJK fonts are large and may need unicode-range splitting.

Pages Router. next/font works in both routers; apply the class in _app.

Tailwind. Map the CSS variables in the Tailwind theme's fontFamily configuration.

FAQ

Does next/font make requests to Google at runtime?

No. Fonts are downloaded at build time and served from your own deployment.

Why do I still see a small shift?

Adjusted fallbacks match metrics closely but not perfectly; kerning and specific glyph widths differ. Remaining shifts are usually tiny.

Should I use font-display: optional?

If layout stability matters more than always showing the brand font on first visits, yes. Otherwise use swap with the adjusted fallback.

Can I use variable fonts?

Yes, and they are recommended: one file covers all weights.

Does next/font preload every font?

By default it preloads fonts used in the layout or page. Set preload: false for fonts that are not needed early.

How do I use next/font with CSS modules?

Expose a CSS variable with the variable option, apply the class on a root element, and reference the variable in your CSS.

Does next/font help LCP?

Yes, when the LCP element is text: removing third-party font CSS and preloading font files lets text render sooner.

Can I subset fonts further?

Choosing subsets is supported; for custom fonts, subset the files yourself with tools like glyphhanger or fonttools before using next/font/local.