Next.js can automatically optimize font loading for you, and it’s doing more than just making your page look good; it’s actively preventing layout shifts and speeding up perceived load times.
Let’s see it in action. Imagine you have a simple app/page.js with a custom font imported.
import localFont from 'next/font/local';
const myFont = localFont({ src: './my-font.woff2' });
export default function Page() {
return (
<main className={myFont.className}>
<h1>Hello, World!</h1>
<p>This text will use our custom font.</p>
</main>
);
}
When Next.js builds this, it doesn’t just link to your font file. It analyzes its usage, generates @font-face rules, and even preloads the font files so the browser can fetch them as early as possible. This means the browser doesn’t have to wait for your CSS to parse and then discover the font; it’s often already on its way.
The core problem Next.js is solving here is the "Flash of Unstyled Text" (FOUT) or "Flash of Invisible Text" (FOIT). Without optimization, a browser might render your page with a default fallback font, then download your custom font, and then swap it in. This swap can cause the text to reflow, shifting the layout and potentially jiggling content around your users. Alternatively, it might wait for the font to download entirely before rendering any text, leading to a blank page for a bit. Next.js aims to minimize or eliminate both.
Here’s how it works under the hood:
-
Automatic
FontFaceObject Generation: When you import a font usingnext/font, Next.js automatically generates the necessaryFontFaceobject for the font. This object contains all the metadata about the font, including its path, weight, and style. -
@font-faceCSS Generation: Next.js then generates the corresponding@font-faceCSS rules. These rules tell the browser how to fetch and use the font. For example, it might produce something like this in your CSS:@font-face { font-family: 'My Font'; src: url('/_next/font/local/my-font.woff2?v=12345') format('woff2'); font-weight: 400; font-style: normal; font-display: optional; /* Or swap, block, etc. */ } -
Preloading: Crucially, Next.js adds
<link rel="preload">tags to your HTML’s<head>. These tags instruct the browser to start downloading the font files as soon as possible, even before the main CSS is fully parsed. This dramatically reduces the time it takes for the font to become available.<link rel="preload" href="/_next/font/local/my-font.woff2?v=12345" as="font" type="font/woff2" crossorigin="" /> -
font-displayStrategy: By default,next/fontusesfont-display: optional. This is a clever strategy. If the font is available very quickly (which preloading helps ensure), it’s used. If it takes too long, the browser falls back to a system font and never swaps. This guarantees no layout shift, prioritizing stability over the custom font if the download is slow. You can override this behavior withdisplay: 'swap'if you prefer the font to swap in, even if it causes a minor shift, to ensure your custom font is always used.
You can control the font-display strategy when defining your font:
import localFont from 'next/font/local';
const myFont = localFont({
src: './my-font.woff2',
display: 'swap', // This will use 'swap' instead of 'optional'
});
export default function Page() {
return (
<main className={myFont.className}>
<h1>Hello, World!</h1>
</main>
);
}
The weight and style properties also allow you to define multiple variations of a font, and Next.js will generate separate @font-face rules and preloads for each, ensuring efficient loading for all your font needs.
import localFont from 'next/font/local';
const myFont = localFont({
src: [
{
path: './my-font-bold.woff2',
weight: '700',
style: 'normal',
},
{
path: './my-font-italic.woff2',
weight: '400',
style: 'italic',
},
],
display: 'swap',
});
When Next.js optimizes fonts, it automatically hashes the font filenames. This cache-busting mechanism ensures that when you update a font file, users will download the new version. For example, instead of my-font.woff2, you might see /fonts/my-font.a1b2c3d4.woff2. This is handled entirely by the build process.
A common pitfall is forgetting to apply the className prop from the imported font object to the HTML element that needs to use it. If you import myFont but forget className={myFont.className} on your body, html, or a specific container, the font styles simply won’t be applied, and you’ll see the default system font.
The next thing you’ll likely encounter is managing a large number of fonts or complex font loading strategies, which might lead you to explore advanced configurations or third-party libraries for more granular control.