You picked the perfect typeface. Your site looks gorgeous in Figma and stunning in production. Then Lighthouse reports LCP at 3.8 seconds and your stomach drops.
The culprit isn’t your images. It’s not your JavaScript bundle either — you already tracked that down. It’s your fonts. Custom web fonts are one of the most common invisible LCP killers because they block text rendering, and text is frequently the largest contentful element on the page. Font loading performance optimization isn’t glamorous, but it might be the single fastest way to get your LCP under the 2.5-second threshold.
By the end of this, you’ll know exactly which technique to reach for and why.
Why Fonts Tank Your LCP Score
Here’s the part most developers don’t think about: your LCP element is probably text.
Not a hero image. Not a video thumbnail. Your h1 or your first paragraph — the biggest visible thing when the page loads. And if that text relies on a custom font, the browser has a decision to make.
Default behavior in most browsers: wait up to 3 seconds for the custom font to download before showing anything. That’s FOIT — Flash of Invisible Text. During that wait, your text is invisible. Invisible text can’t be the Largest Contentful Paint. Your LCP clock keeps ticking while your user stares at a blank space where your headline should be. Safari has shortened its block period recently, but Chrome and Firefox still default to that full 3-second window.
The math gets ugly fast. DNS lookup to your font host (~50ms) plus TLS handshake (~100ms) plus the actual font download (~200–600ms) plus render time. That’s potentially 800ms burned on typography alone — and the 2.5-second LCP threshold doesn’t leave much room. If your site is already slow for other reasons, fonts push you over the cliff.
So fonts are blocking your LCP. But you’re not dropping your brand typeface. What do you actually do?
The font-display Decision: Swap, Optional, or Fallback
One CSS property changes everything. font-display tells the browser what to do while the custom font is still loading — and picking the right value is the highest-leverage font loading performance optimization you can make.
font-display: swap — shows fallback text immediately, then swaps in the custom font when it arrives. Your LCP happens fast because text renders right away with a system font. The trade-off: when the custom font loads and replaces the fallback, the different metrics cause a layout shift. Better LCP, worse CLS.
font-display: optional — gives the font roughly 100ms to load. If it misses that window, the browser uses the fallback for the entire page visit. No swap means no layout shift, so CLS stays clean. The trade-off: first-time visitors on slow connections may never see your brand font at all.
font-display: fallback — the middle ground. Short block period (~100ms of invisible text), then fallback, then swap if the font arrives in time. Less CLS risk than swap, more likely to show the custom font than optional.
Here’s the decision framework:
/* You're failing LCP and text is the LCP element → swap */
@font-face {
font-family: 'YourFont';
src: url('/fonts/yourfont.woff2') format('woff2');
font-display: swap;
}
/* You're passing LCP but failing CLS → optional */
@font-face {
font-family: 'YourFont';
src: url('/fonts/yourfont.woff2') format('woff2');
font-display: optional;
}
Choose swap if your Lighthouse LCP is red and text is the culprit. Choose optional if your LCP is fine but CLS keeps spiking. Choose fallback if you want a compromise and can accept the risk of either.
Five minutes of work. Biggest single improvement to reduce font load time impact on your Core Web Vitals. But you’re still waiting for a font file to download from somewhere — is there more you can shave off?
Preload, Self-Host, and Stop Wasting Milliseconds
font-display changes how the browser handles a slow font. Preloading and self-hosting make the font arrive faster.
Preload your critical font. Add this to your <head>:
<link rel="preload" href="/fonts/yourfont.woff2"
as="font" type="font/woff2" crossorigin>
That crossorigin attribute is mandatory — even for same-origin fonts. This is the number one preload mistake. Without it, the browser fetches the font twice: once from the preload (which it discards because the CORS mode doesn’t match) and once from the @font-face rule. Your “optimization” silently doubles the work. Preloading critical fonts can save 200–800ms on LCP when done correctly.
Important: preload one or two font files, not five. Preload only the weight and style used above the fold — usually your regular 400 weight. Preloading everything congests the browser’s request queue and delays other critical resources. That defeats the purpose.
Self-host your fonts. If you’re loading from Google Fonts, every page visit starts with a DNS lookup and TLS handshake to fonts.googleapis.com — that’s 100–300ms before a single byte of font data moves. Google Fonts lost its cross-site caching advantage years ago when browsers partitioned their caches. Self-hosting eliminates that round trip entirely.
WOFF2 only. It’s roughly 30% smaller than WOFF and supported by every browser that matters. Drop WOFF and TTF fallbacks unless your analytics show meaningful IE11 traffic. They don’t.
Subset ruthlessly. If your site is English-only, you don’t need Cyrillic, Greek, or Vietnamese character ranges. Tools like glyphhanger or fonttools can strip unused glyphs and cut file size by 50–70%. Smaller file means faster download means better LCP. That’s the entire game.
You’ve optimized delivery. But if you’re loading four separate font files for regular, bold, italic, and bold italic — there’s a way to cut that down to one.
Variable Fonts and the CLS Fix Nobody Talks About
Variable fonts pack multiple weights and styles into a single file. Instead of loading Inter-Regular.woff2, Inter-Bold.woff2, Inter-Italic.woff2, and Inter-BoldItalic.woff2 as four separate requests, you load one Inter-Variable.woff2. The variable file is larger than any single static file (~270KB vs ~150KB), but it’s far smaller than four of them combined (~600KB). Fewer HTTP requests, smaller total payload, better LCP.
If your site uses three or more weights of the same family, the variable fonts performance advantage is straightforward. One request instead of four. The browser decompresses once, parses once, renders from one cached file. For a performance-sensitive site, it’s an easy win.
Now, the technique almost nobody covers.
If you chose font-display: swap, you accepted layout shift as the price of faster LCP. But you can eliminate most of that shift with three CSS descriptors that match your fallback font’s dimensions to your custom font:
@font-face {
font-family: 'YourFont Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 95%;
descent-override: 22%;
}
body {
font-family: 'YourFont', 'YourFont Fallback', sans-serif;
}
size-adjust, ascent-override, and descent-override tweak the fallback font’s metrics to match your custom font. When the swap happens, the text occupies nearly the same space — so the layout barely shifts. You get the LCP benefit of swap without the CLS penalty. These descriptors are now widely supported across modern browsers, and they’re the closest thing to a free lunch in web font loading strategies.
You’ve got all the techniques. But what order do you ship them in?
The Priority Order: Ship This Today
Your fancy type was killing your LCP. Now you know exactly why — and you have five fixes ranked by impact per minute of effort.
Do these in order. Stop when your LCP passes:
- Add
font-display: swap(oroptional). Five minutes. Biggest single impact. - Preload your primary font file with the
crossoriginattribute. Two minutes. - Self-host and serve WOFF2 only. Thirty minutes, including subsetting.
- Switch to a variable font if you’re loading three or more weights. One hour.
- Add
size-adjustand override descriptors if CLS spikes after step 1. Fifteen minutes.
The honest take: most sites pass LCP with just steps 1 and 2. A five-minute font-display change and a two-line preload hint. The remaining steps are for when you’ve already handled the obvious performance issues and you’re chasing the last few hundred milliseconds.
You don’t need all five. Do the minimum that gets you under 2.5 seconds, then move on to whatever’s actually slowing things down. Font loading performance optimization is one piece of a bigger puzzle — but it’s the piece most developers overlook until Lighthouse yells at them.
The best font loading strategy is the one you actually ship. Go make your fonts fast and move on. That’s the whole point.