Typography and Web Fonts in Next.js, next/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