Taming custom fonts without wrecking performance

Typography sets the character of a site, but custom fonts come with a cost. Poorly loaded font files trigger Cumulative Layout Shift (CLS) and flashes of unstyled content (FOUC), both of which degrade the browsing experience and can hurt search rankings. Vercel hit these problems on its own marketing sites and worked around them with a stack of manual optimizations. With Next.js 13, the team replaced that machinery with next/font, which bundles the best practices into a single API.

The old approach: manual font gymnastics

Before next/font, Vercel's sites achieved clean font loading through a combination of techniques:

  • Self-hosting font files rather than pulling from a third-party CDN like Google Fonts
  • Preloading fonts with <meta> tags
  • Preloading only the Latin character subset, letting other subsets load on demand
  • Choosing fallback fonts that closely matched the custom font's metrics, even if they weren't universally available
  • Setting font-display: optional to prevent late-loading fonts from causing visual flicker
  • Using client-side JavaScript to detect first-time visitors and serve them fallback fonts for zero layout shift

Each of these required deliberate code. The font configuration lived in a custom next/head component rendered on every page:

<script

dangerouslySetInnerHTML={{

__html: `

if (!window.newVisit && document.cookie && document.cookie.indexOf('beenHere=1') !== -1) {

document.documentElement.classList.add('inter')

} else {

window.newVisit=true

}

setTimeout(function() {

document.cookie='beenHere=1;samesite=lax;expires='+

new Date(Date.now()+31*3600*24*1000).toGMTString()

})`,

}}

/>

<link

as="font"

crossOrigin="anonymous"

href="https://assets.vercel.com/raw/upload/v1660068731/fonts/4/Inter.var.latin.woff2"

rel="preload"

type="font/woff2"

/>

The accompanying CSS defined the @font-face rules:

:root {

--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',

'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',

sans-serif;

}

:root.inter {

--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',

'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',

sans-serif;

}

Manual subsetting added another layer of complexity:

@font-face {

font-family: 'Inter';

font-style: normal;

font-weight: 100 900;

font-display: optional;

src: url('https://assets.vercel.com/raw/upload/v1660068731/fonts/4/Inter.var.latin.woff2')

format('woff2');

unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,

U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,

U+2215, U+FEFF, U+FFFD;

}

/* Plus eight more @font-face definitions for other languages and characters. */

Adding any new font meant extending this fragile setup across multiple files.

What next/font changes

next/font collapses those optimizations into a compact configuration. The core win is automatic self-hosting, which removes external network requests entirely. The font files themselves are also trimmed so only the characters your site actually uses are preloaded.

The most significant improvement is how layout shift is handled. next/font reads the actual font file (a .ttf or .woff2, for instance) and calculates the size-adjust property server-side, before the font is ever requested. This lets it generate a fallback font with spacing that matches the custom font, so the swap is seamless when the real font loads.

Vercel pairs this with display: "swap" in their font configuration, which lets the browser exchange the fallback for the custom font as soon as it's ready—even if loading is slow.

The result: dramatically less code

Here's the entirety of Vercel's current font setup:

import { Inter } from 'next/font/google';

const interFont = Inter({

display: 'swap',

subsets: ['latin'],

variable: '--font-sans',

});

export const interFontClass = interFont.variable;

The variable property is key here—it exposes the font as a CSS variable, so existing styles keep working without changes. The same pattern works for integrating with TailwindCSS or adding new fonts to a project.