If you've used Next.js in the last couple of years you've probably reached for next/font. It's genuinely great — but I still see custom fonts done in ways that tank the Lighthouse score or make the page jump around on load. Here's how I actually wire up fonts in a Next.js app now, and the one thing that quietly fixes most of the layout shift.
Google fonts: next/font/google already self-hosts
First, a thing a lot of people miss: next/font/google doesn't load anything from Google's servers at runtime. At build time Next downloads the font files and serves them from your own origin. So you get the self-hosting win (no third-party request on the critical path) for free:
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], display: "swap" });
export default function RootLayout({ children }) {
return <html className={inter.className}><body>{children}</body></html>;
}
That subsets: ["latin"] line matters — it subsets the file so you're not shipping glyphs for languages you don't use.
Custom / paid fonts: next/font/local
For a font that isn't on Google Fonts — a brand font, a freebie you downloaded, whatever — use next/font/local and point it at a WOFF2 in your project:
import localFont from "next/font/local";
const display = localFont({
src: [
{ path: "./fonts/MyFont-Regular.woff2", weight: "400", style: "normal" },
{ path: "./fonts/MyFont-Bold.woff2", weight: "700", style: "normal" },
],
display: "swap",
variable: "--font-display",
});
Then expose it as a CSS variable and use it wherever:
<html className={display.variable}>
.heading { font-family: var(--font-display), sans-serif; }
If your font is a .ttf/.otf, convert it to WOFF2 first — smaller and supported everywhere. I usually just drop it into FontBoxDL's webfont generator (browser-based, no install) and grab the WOFF2 back. And if you're still hunting for the font itself, the free library has a pile of them.
The thing that actually fixes layout shift
Here's the part people skip. Even with display: swap, when the real font swaps in it usually has different metrics than the fallback — so text reflows and your CLS spikes. next/font can fix this automatically if you give it a fallback to match against:
const display = localFont({
src: "./fonts/MyFont-Regular.woff2",
display: "swap",
fallback: ["system-ui", "arial"],
adjustFontFallback: "Arial", // generates a size-adjusted @font-face for the fallback
});
adjustFontFallback makes Next emit a fallback @font-face with size-adjust/ascent-override tuned so the fallback occupies almost exactly the same space as your real font. The swap becomes nearly invisible. This one prop has done more for my CLS numbers than anything else.
Quick gotchas
- Don't import a font inside a component that re-renders. Declare it at module scope (top of the file), once. Importing it in the render path defeats the optimization.
-
variablevsclassName: useclassNameif it's your single global font; usevariablewhen you want multiple fonts available as CSS custom properties. -
Preload is automatic for fonts used on the route — you don't need a manual
<link rel="preload">like you would in a plain HTML setup.
TL... actually, no TL;DR
Use next/font/google for Google fonts (it self-hosts), next/font/local for everything else, ship WOFF2, and set adjustFontFallback so the swap doesn't shove your layout around. Takes ten minutes and your Core Web Vitals will thank you.
What's your go-to for custom fonts in Next — next/font/local, or do you still hand-roll the @font-face?






















