TL;DR
Loading fonts from Google Fonts via <link> in <head> in 2026 is an automatic performance regression, extra DNS lookup, render-blocking CSS, and CLS when text "jumps" after weight 600 loads. next/font self-hosts files in the bundle, sets font-display: swap, and generates CSS variables, no request to fonts.googleapis.com. Subsetting to latin + latin-ext (Polish ą, ę, ł and German umlauts) cuts 60–80% of file size vs full unicode. Space reservation via size-adjust or fixed hero line-height prevents layout shift. Hosting on DevStudioIT Cloud serves fonts from the same origin as HTML, one TLS handshake, long-term cache after build.
Who is this for
- Corporate sites with custom Figma typography, dev must match without hurting LCP
- Next.js teams seeing CLS 0.15+ in Lighthouse despite optimized images
- Multilingual PL/DE projects with latin-ext, full charset without unused CJK glyphs
- Designers picking 4 font weights "because it looks good", KB budget needs justification
- Anyone after a CWV audit where "font loading" appears in diagnostics
Keyword
next/font subsetting, web fonts performance, cls fonts nextjs, font-display swap, corporate site typography, google fonts self hosting 2026
The problem: external fonts and Core Web Vitals
Classic import:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">Request chain:
| Step | Delay | Impact |
|---|---|---|
| DNS fonts.googleapis.com | 20–80 ms | After HTML TTFB |
| CSS font-face | blocking or FOIT | Delayed first text |
| DNS fonts.gstatic.com | another RTT | Worse LCP when text is LCP |
| Download .woff2 400 + 600 + 700 | 150–400 KB | Main thread decode |
CLS happens when fallback (Arial) metrics differ from Inter, hero heading height changes 4–12 px after swap.
next/font, self-hosting in Next.js App Router
// app/fonts.ts
import { Inter } from 'next/font/google';
export const inter = Inter({
subsets: ['latin', 'latin-ext'],
weight: ['400', '600'],
display: 'swap',
variable: '--font-inter',
preload: true,
adjustFontFallback: true,
});Layout:
import { inter } from './fonts';
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body className="font-sans antialiased">{children}</body>
</html>
);
}Tailwind (tailwind.config.ts):
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
},
},
},| Option | Effect |
|---|---|
subsets: ['latin-ext'] |
Polish and German diacritics |
weight: ['400','600'] |
Only used weights, each weight = separate file |
adjustFontFallback: true |
Automatic size-adjust on fallback |
variable |
One CSS class, no scattered @font-face |
Subsetting, what to include, what to skip
| Subset | When | Approx Inter 400 size |
|---|---|---|
latin |
EN-only landing | ~15 KB woff2 |
latin-ext |
PL, DE, CS, RO | +8–12 KB |
cyrillic |
UA/RU market | +20 KB, only if locale |
| Full unicode | ❌ avoid | 200 KB+ |
For DevStudio.it (pl/en/de) ['latin', 'latin-ext'] is enough. Do not import vietnamese "just in case".
Licensed brand font (OTF):
import localFont from 'next/font/local';
export const brandSans = localFont({
src: [
{ path: '../public/fonts/Brand-Regular.woff2', weight: '400' },
{ path: '../public/fonts/Brand-SemiBold.woff2', weight: '600' },
],
display: 'swap',
variable: '--font-brand',
});Convert OTF → woff2: fonttools or CI pipeline before commit.
CLS, space reservation beyond next/font
adjustFontFallback helps, but hero with large text-5xl and custom line-height can still shift:
.hero-title {
font-size: clamp(2rem, 5vw, 3.5rem);
line-height: 1.15;
min-height: 2.3em; /* reserve for 2 lines */
}| Technique | CLS | Notes |
|---|---|---|
font-display: optional |
Lowest | Risk of no custom font on slow network |
swap + size-adjust |
Low | B2B recommendation |
| Preload only critical weight | Better LCP | preload: true in next/font by default |
| System font stack on mobile | Zero CLS | Brand vs metrics tradeoff |
Test mobile 4G throttling in Lighthouse, desktop hides the problem.
Typography and LCP, when LCP is text
On pages with minimal hero the LCP element may be H1 heading, not an image. Then:
- Preload only weight used in H1 (400 or 600)
- Avoid
@importfonts in CSS modules, loads late - Server Component for hero, HTML with font in first stream bytes
Font should not live in a lazy-loaded Client Component, that delays text LCP.
Multiple fonts, heading + body without KB explosion
| Pattern | woff2 files | Recommendation |
|---|---|---|
| 1 family, 2 weights | 2 × ~20 KB | ✅ default choice |
| Body + display (2 families) | 4 files | OK if display only H1–H2 |
| 4 weights + 2 families | 8+ files | Review in performance budget |
Display font on headings via next/font with preload: false if H1 is below fold on mobile, controversial; better to shrink display subset.
Monitoring after deploy on DevStudioIT Cloud
After deploy compare:
| Source | Font-related metric |
|---|---|
| Lighthouse CI | CLS, LCP, "Font display" audit |
| CrUX / Search Console | Field CLS after 28 days |
| Web Vitals RUM | layout-shift attributions |
Regression: designer added weight 700 and italic "for quotes", bundle +40 KB, CLS +0.05. Performance budget in CI should catch via overall score; assert CLS directly.
Design → dev handoff, Figma typography without surprises
Before the developer implements fonts from mockups, agree on a decision table:
| Figma element | Next.js implementation | Question for design |
|---|---|---|
| Inter 400 body | next/font weight 400 |
Is it enough? |
| Inter 600 headings | weight 600 | Is 700 necessary? |
| Inter 700 CTA | Separate woff2 file | Is 600 enough on button? |
| Italic quotes | +italic file | System <em> instead? |
Figma design tokens should map 1:1 to Tailwind fontFamily, avoid "Satoshi in Figma, Arial in production because of licensing." If brand font needs web license, woff2 in repo before sprint start, not on go-live eve.
Transfer Figma line-height (e.g. 120%) to CSS with min-height reserve for multi-line H1, Figma does not measure CLS.
Typography and accessibility, not just KB
| Requirement | Minimum | Font impact |
|---|---|---|
| WCAG AA contrast | 4.5:1 body, 3:1 large text | Avoid weight 300 on light background |
prefers-reduced-motion |
No text animation | Do not animate font-weight |
| 200% zoom | Readable without horizontal scroll | rem instead of fixed px on font-size |
Custom font does not excuse low contrast, pick weight 500/600 instead of 300 on gray background.
FAQ
next/font google vs self-host files in public/?
next/font/google downloads at build and self-hosts, equivalent to manual public/, but with automatic hash in filename and zero CORS config. Manual public/ only for non-Google fonts (brand OTF).
Are variable fonts (.woff2 variable) always smaller?
Often yes with 3+ weights, one file instead of three. Not every family has a well-hinted variable version; test size and render on Windows.
Third-party CDN fonts (Adobe Fonts, Fontshare)?
Each external origin means separate DNS + cache. For CWV self-host after licensing. Adobe Fonts on marketing pages with strict LCP budget, risky.
Inter vs system-ui, when to drop custom?
Ads landing with LCP > 3 s on mobile, test variant with system-ui. If conversion is unchanged, keep system stack.
Want typography without CLS regression?
- Contact us, we pick font stack, next/font, and Lighthouse CI thresholds
- Performance budget CWV, enforce CLS in PRs
- Next.js image optimization, when LCP is hero image, not font
About the author
We build fast websites, web/mobile apps, AI chatbots and hosting setups — with a focus on SEO and conversion.
Recommended links
From theory to production — Branchly, our hosting stack and shipped work.
