Typography and Web Fonts in Next.jsnext/font, Subsetting, and CLS Without Regression (2026)

typography6 min readJuly 20, 2026

Author: DevStudio.it

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: [&#39;latin-ext&#39;] Polish and German diacritics
weight: [&#39;400&#39;,&#39;600&#39;] 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) [&#39;latin&#39;, &#39;latin-ext&#39;] 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:

  1. Preload only weight used in H1 (400 or 600)
  2. Avoid @import fonts in CSS modules — loads late
  3. 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 &lt;em&gt; 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?

Related posts

Performance Budget and Core Web Vitals for Teams — Lighthouse CI in Practice (2026)
5 min read
Core Web Vitals – Measuring UX and Page Performance in 2026
10 min read
Video Hero and LCP — When Autoplay Hurts Corporate Site Performance (2026)
6 min read

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.

Like how we think? Let's build something together.

Start project configuration