Guide
Web font optimization explained: WOFF2, font-display, and Core Web Vitals
Custom typography is one of the fastest ways to make a site feel distinctive — and one of the easiest ways to wreck Core Web Vitals. A single 200 KB font file can delay Largest Contentful Paint (LCP) when it competes with your hero image, and a late font swap can shift headings enough to fail Cumulative Layout Shift (CLS). The fix is not "never use custom fonts." It is treating fonts like any other critical asset: pick efficient formats, load only what you need, declare how the browser should behave while files download, and match fallbacks so the swap is invisible. This guide walks through those decisions for static sites, SPAs, and content publishers who care about both design and HTTP caching.
Why fonts hurt performance
Browsers discover fonts late in the page load unless you tell them otherwise. A
typical flow: HTML arrives, CSS is parsed, the stylesheet references
@font-face rules or a Google Fonts URL, then the browser
fetches font binaries. If your LCP element is a headline set in that custom face,
LCP waits on the font download chain. Even when text renders with a system
fallback first, swapping to the web font often changes glyph widths — lines
reflow, buttons resize, and CLS spikes.
Two historical terms describe the visible delay:
FOIT (flash of invisible text) when the browser hides text until
the font loads, and FOUT (flash of unstyled text) when fallback
text appears then swaps. Modern font-display values let you choose
which trade-off you accept. The goal of optimization is to minimize both the wait
and the layout jump.
What to measure
In Chrome DevTools Performance and Lighthouse, watch LCP element timing and layout shift entries tagged with font loads. In the field, CrUX reports aggregate LCP and CLS — font fixes that help lab scores but not real users usually mean caching or CDN issues. Pair font work with CDN edge caching so repeat visitors do not re-download unchanged files.
Choose the right format and weights
WOFF2 is the default choice in 2026. It uses Brotli compression
and is supported by every modern browser. Ship WOFF2 only unless you must support
very old clients — in that rare case add WOFF as a fallback inside
@font-face, not TTF or OTF directly (those are larger and parse
slower).
Load fewer files
Each weight and style you request is a separate download. A page that imports
Inter 300, 400, 500, 600, 700, and 800 from Google Fonts may pull six files even
if body text only uses 400 and headings use 600. Audit actual usage in your CSS and
delete unused weights. For UI chrome, system UI stacks
(system-ui, sans-serif) are often faster than another web font family.
Variable fonts
A variable font encodes a weight axis (and sometimes width or
slant) in one file. Instead of separate 400 and 700 binaries, you load one file
and set font-weight: 450 or animate weight smoothly. The file can be
larger than a single static weight but smaller than three or four separate cuts —
a net win when you need many weights. Check the font license; not every family
offers a variable release.
font-display: swap, optional, and the rest
The font-display descriptor in @font-face (or the
display= parameter on Google Fonts URLs) controls the first-render
behavior:
- swap — show fallback immediately, swap when the font arrives. Best default for body text and content sites; may cause a brief FOUT but avoids invisible text.
- optional — use the web font only if it loads within a short window (~100 ms); otherwise stick with fallback for this page view. Excellent for decorative faces where system fonts are acceptable.
- block — short invisible period then swap. Rarely appropriate; hurts LCP on slow networks.
- fallback — invisible for ~100 ms, then fallback, then swap. A compromise few teams need once
swapand size-adjust exist.
For publisher sites where readability matters, use font-display: swap
on text faces and optional on display fonts used only above the fold
accents. Pair swap with fallback matching (below) so the swap does not move layout.
Preload critical fonts — carefully
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
tells the browser to start downloading the font at high priority alongside CSS.
Preload only the one or two files needed for above-the-fold LCP text — usually a
regular and semibold cut, or one variable file. Over-preloading competes with
hero images and JavaScript, hurting overall LCP.
Preload URLs must exactly match the @font-face src URL,
including query strings. If you preload from a CDN hostname, the CSS must reference
the same hostname or the browser downloads twice. Add long cache lifetimes on font
files (see
Cache-Control for static assets)
because fonts change rarely.
preconnect vs preload
rel="preconnect" to fonts.gstatic.com only helps when
you still load from Google Fonts. It opens TCP/TLS early but does not fetch the
font. Self-hosting removes that third-party hop entirely — often the bigger win for
privacy, predictability, and
CSP simplicity.
Subsetting and self-hosting
A full Latin Inter font might be 90 KB WOFF2; a subset with only characters
you use (Basic Latin plus a few symbols) can halve that. Tools like
glyphhanger, fonttools/pyftsubset, or build plugins in Next.js and
Astro generate subsets from your HTML/CSS content. For multilingual sites, ship
separate subsets per script (Latin, Cyrillic, CJK) and load the right one per
locale — do not send a 2 MB CJK font to English readers.
Self-host vs Google Fonts
Google Fonts is convenient: one CSS import, global CDN, automatic
font-display. Downsides: extra DNS connection, privacy considerations,
and CSS that may pull more weights than you intended. Self-hosting means you copy
WOFF2 files into /fonts/, define explicit @font-face
rules, and cache them on your origin or CDN. Build pipelines can download and
subset fonts at compile time so production never depends on Google's CSS endpoint.
Either approach works if you subset, limit weights, and set
font-display: swap. Self-hosting shines when you already fingerprint
assets (inter-abc123.woff2) for immutable caching — the same pattern
used for optimized images in
web image pipelines.
Fallback font matching (size-adjust)
CLS from font swap happens because Arial and your custom sans-serif have different
metrics — different x-height, cap height, and default advance widths. CSS
size-adjust, ascent-override,
descent-override, and line-gap-override on the
fallback @font-face let you tune a system font until it occupies nearly
the same box as the web font.
The workflow: load the web font in Fontaine, @next/font, or the fallback-font-metrics npm package to compute overrides, then emit a fallback face:
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
Use font-family: "Inter", "Inter Fallback", sans-serif; in your stack.
When Inter loads, the swap is visually subtle and CLS stays near zero. This is one
of the highest-leverage CLS fixes for content sites and pairs well with responsive
layouts built in
Flexbox and Grid.
Rendering strategies and SPAs
In client-rendered apps, fonts requested only after JavaScript hydrates arrive even
later. If SEO or first paint matters, inline critical @font-face rules
in the document head or server-render them — see
SSR vs CSR trade-offs.
Icon fonts (legacy Font Awesome webfont builds) are usually worse than inline SVG
icons: they block text rendering, are bad for screen readers, and rarely subset well.
Prefer SVG sprites or icon components.
Third-party embeds
Widgets, ad scripts, and chat bubbles often inject their own font CSS. You cannot control their weight, but you can defer non-critical widgets below the fold so they do not contend with your LCP font on the network waterfall. Reserve fixed height for ad slots to avoid CLS when those widgets load their typography.
Practical checklist
- WOFF2 only — drop legacy formats unless analytics prove you need them.
- Two weights max for most content sites, or one variable font file.
- font-display: swap on body/heading faces; optional on decorative accents.
- Preload only the LCP font file; verify no duplicate downloads in Network panel.
- Subset to characters and scripts you actually serve.
- Self-host when you want immutable caching and fewer third-party connections.
- size-adjust fallbacks to eliminate swap layout shift.
- Cache fonts for a year with fingerprinted filenames; purge on deploy when hashes change.
- Re-test LCP and CLS on slow 4G throttling after every font change.
Typography is part of performance engineering, not an exception to it. A fast site with a tuned font stack reads as more trustworthy — which matters whether you are shipping a product landing page or a long-form guide that monetizes through organic search.
Related reading
- Core Web Vitals explained — LCP, INP, and CLS thresholds publishers must hit
- Web image optimization explained — formats and responsive markup that compete with fonts for LCP
- HTTP caching explained — long-lived cache headers for fingerprinted font files
- CSS Flexbox and Grid layout explained — stable layouts that survive font metric changes