Web Fonts and Performance: How Not to Kill LCP with a Custom Font
Custom fonts are the leading cause of CLS and LCP failures. We break down font-display strategies, preload, self-hosting, and the Font Loading API for premium typography without the performance cost.
Every designer wants premium typography. Every developer plugs in Google Fonts via <link>. And so your site loads great — images, JS, CSS all fly in — yet the user stares at a white screen for another two seconds waiting for the font. LCP is in the red zone, Core Web Vitals are blown, rankings have dropped.
In this article — how to use custom fonts without killing performance.
How the browser handles fonts
The standard font loading sequence:
- The browser parses HTML, builds the DOM
- Parses CSS, finds
@font-faceor<link rel="stylesheet">referencing the font - Only now requests the font file
- Downloads the font (typically 50–200 KB)
- Parses the font, renders text
Time passes between steps 1 and 5. On a fast connection — 200–500 ms; on a slow one — 2–4 seconds. Throughout that window your text is either invisible (FOIT — Flash Of Invisible Text) or rendered in a system fallback and then "jumps" to the custom font (FOUT — Flash Of Unstyled Text).
FOIT kills LCP — no text means no LCP candidate. FOUT kills CLS — text "jumps" because the fallback and the custom font have different metrics.
Strategy 1: font-display
The CSS property font-display controls behaviour during loading. Values:
auto— the browser decides, usually FOITblock— invisible text until the font loads (3 sec), then swapswap— immediate system fallback, then swap to the custom font (causes CLS)fallback— a compromise: 100 ms of invisibility, then swap, then fix after 3 secoptional— the most conservative: 100 ms of invisibility, then permanent fallback (if the font didn't arrive in time)
Recommendation: swap for most sites (best UX); optional if CLS is critical and it is acceptable to occasionally skip the custom font.
@font-face {
font-family: 'General Sans';
src: url('/fonts/general-sans-medium.woff2') format('woff2');
font-weight: 500;
font-display: swap;
}
Strategy 2: preload critical fonts
font-display: swap solves FOIT, but FOUT remains — the user first sees the system font, then the replacement. On a main hero block this is jarring.
The solution is to preload critical fonts:
<head>
<link rel="preload"
href="/fonts/general-sans-medium.woff2"
as="font"
type="font/woff2"
crossorigin>
</head>
What this does: the browser starts downloading the font immediately after HTML, without waiting for CSS to be parsed. The font typically arrives by the time text needs to be rendered — FOUT does not occur.
Important: the crossorigin attribute is required on the preload even for self-hosted fonts. Without it the browser will make a second request for the font, making the preload useless.
What to preload:
- All weights used above the fold (typically 400 + 600, or 500 + 700)
- Do not preload every variant — this pollutes network priority
Strategy 3: self-hosting instead of Google Fonts
Google Fonts via <link> has three drawbacks:
- An extra DNS lookup to
fonts.googleapis.com, then tofonts.gstatic.com— 2 connection setups - Dependency on an external service — if Google Fonts goes down (it has happened), your font goes down with it
- GDPR risk — Google Fonts may log user IP addresses, which constitutes personal data
Self-hosting (putting font files on your own server) solves all of this:
# Download files from https://gwfh.mranftl.com/fonts (Google Fonts Helper)
# Place them in public/fonts/
In CSS:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
Add HTTP/2 from your own domain — one TCP connection, all fonts over a single channel.
Strategy 4: variable fonts
A custom font typically ships as many files — one per weight per style. 12 files = 12 requests = 12 downloads.
Variable fonts are a single file with all weights encoded inside. The browser interpolates between them. One 80 KB file instead of 12 files at 30 KB each — savings in both traffic and request count.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
}
/* Usage */
h1 { font-weight: 700; }
p { font-weight: 400; }
.lead { font-weight: 500; }
Variable font support — all modern browsers. Available since 2018.
Strategy 5: matching the fallback to the custom font
FOUT (the font jump) can be minimised if the system fallback has similar proportions to the custom font.
CSS properties for this: size-adjust, ascent-override, descent-override, line-gap-override inside @font-face.
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
size-adjust: 107.4%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter-fallback', system-ui, sans-serif;
}
Effect: while Inter is loading, text is rendered using Arial with adjusted proportions — it looks almost identical to Inter. When the real Inter loads, the swap happens with no visible jump.
Utility for calculating overrides: Fontkit or the Capsize CSS generator.
Strategy 6: subsetting
A full font file contains every character — Latin, Cyrillic, Greek, Asian glyphs, mathematical symbols, emoji. If your site is in English, you only need Latin plus basic symbols. This can shrink the file from 200 KB down to 60–80 KB.
# Using pyftsubset (Python):
pyftsubset Inter-Regular.ttf \
--unicodes="U+0020-007E,U+0400-04FF,U+0500-052F,U+2000-206F,U+2070-209F,U+20A0-20CF" \
--output-file=Inter-Regular-subset.woff2 \
--flavor=woff2
Or via the Google Fonts API, which already returns a subset:
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap&subset=cyrillic-ext');
What not to do
- Do not load 10 weights if you use only 3. Each weight adds +30–50 KB.
- Do not load custom fonts for print stylesheets or email. It is wasted overhead.
- Do not use the FontFace JS API unless you have a specific reason. In most cases
font-display: swapplus preload handles the task declaratively.
A real-world example
Before optimisation:
- Google Fonts via
<link>, 4 weights - LCP 3.8 sec on 4G mobile
- CLS 0.15 due to FOUT
After:
- Self-hosted Inter Variable woff2 (78 KB), single file
- Preload of one variant
size-adjustoverrides for the fallback- LCP 1.9 sec
- CLS 0.04
Fonts alone delivered −1.9 sec off LCP. No other changes.
Summary
Fonts are the invisible part of a site that can kill performance. The simple rules: self-hosting, variable fonts, preload + font-display: swap, subsetting, fallback with overrides. Implementable in a few hours of work.
If you have a site with custom typography and LCP issues — get in touch. Free performance audit within 2 business days.