Fonts (next/font)
Fonts can quietly wreck CLS. next/font self-hosts fonts, subsets to reduce size, and uses display strategies (swap, optional, block) to prevent layout shift and improve LCP.
Outcome
Fonts loaded via next/font with appropriate display strategy, fallback configuration, and variable fonts for flexibility. Self-hosted and subset for performance, CLS prevention with size-adjust.
Fast Track
- Replace Google Fonts
<link>withnext/font/googleimport. - Configure display strategy:
swap(show fallback, then custom),optional(skip if slow),block(wait briefly). - Use
adjustFontFallbackand variable fonts to reduce requests and prevent CLS.
Hands-On Exercise 3.5
The starter repo uses default system fonts. Your task is to add optimized custom fonts.
Requirements:
- Add
next/font/googleimports toapps/web/src/app/layout.tsx. - Use a variable font (e.g., Inter) to reduce requests and provide weight flexibility.
- Configure
display: 'swap'for immediate text visibility with fallback. - Enable
adjustFontFallbackfor size-matched fallback to prevent CLS. - Subset to
['latin']to reduce font file size. - Apply fonts via CSS variables for Tailwind integration.
Implementation hints:
- Display strategies:
swap: Show fallback immediately, swap when custom loads (recommended).optional: Skip custom font if network slow - use fallback only.block: Wait briefly for custom font, then swap (can delay text).fallback: Brief wait, then fallback, swap when ready.auto: Browser default behavior.
- Variable fonts provide all weights in single file - reduces requests.
- Subset to
['latin']or specific character sets to reduce file size. - Self-hosting automatic - no external requests to Google Fonts.
adjustFontFallbackuses size-adjust to match fallback metrics and prevent CLS.
Try It
-
Verify self-hosting:
- Open Network tab, check no requests to fonts.googleapis.com or fonts.gstatic.com.
- Fonts served from your domain at
/_next/static/media/.
-
Test CLS prevention:
- Throttle network to "Slow 3G" in DevTools.
- Verify text renders with fallback, then swaps to custom font without layout shift.
- CLS should remain < 0.1.
-
Check font loading:
- Inspect
<style>tag in HTML with@font-facedeclarations. - Verify display strategy applied:
font-display: swap. - Check variable font includes multiple weights in single file.
- Inspect
Commit & Deploy
git add -A
git commit -m "feat(advanced): optimize fonts with next/font and fallback"
git push -u origin feat/advanced-font-optimizationDone-When
- DevTools Network tab: no requests to
fonts.googleapis.comorfonts.gstatic.com(fonts self-hosted) - Network tab: font files served from
/_next/static/media/with hash in filename - View page source: find
@font-facewithfont-display: swap(or your chosen strategy) - View page source: font file size reduced (subset to latin): look for WOFF2 files < 100KB
- Throttle to "Slow 3G", reload: text renders immediately in fallback font, then swaps to custom (no blank flash)
- Lighthouse Performance audit: CLS score < 0.1 (no layout shift from font swap)
Solution
Click to reveal solution
Update the layout to import and configure fonts:
import type { Metadata } from 'next'
import { Inter, JetBrains_Mono } from 'next/font/google'
import './globals.css'
// Variable font - all weights in single file
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
adjustFontFallback: true,
})
// Monospace font for code blocks
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap',
})
export const metadata: Metadata = {
title: process.env.NEXT_PUBLIC_APP_NAME || 'Vercel Academy Foundation - Web',
description: 'VAF Web',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="container mx-auto px-4 py-8 font-sans">
{children}
</body>
</html>
)
}Update globals.css to use the font variables with Tailwind 4's @theme inline:
@import 'tailwindcss';
@import '@repo/ui/globals.css';
@theme inline {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono: var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}Local Fonts (Alternative)
If you need to use local font files instead of Google Fonts:
import localFont from 'next/font/local'
const customFont = localFont({
src: [
{
path: './fonts/custom-regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/custom-bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-custom',
display: 'swap',
adjustFontFallback: true,
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={customFont.variable}>
<body className="font-sans">
{children}
</body>
</html>
)
}Then override Tailwind's font variables in your CSS:
@import 'tailwindcss';
@theme inline {
--font-sans: var(--font-custom), ui-sans-serif, system-ui, sans-serif;
}References
Was this helpful?