Skip to content

How to Stop Fonts From Flashing or Disappearing on Load

Unoptimized web fonts cause FOUT and FOIT that hurt LCP and CLS scores. Font-display, preloading, and subsetting fixes for product UI.

· · 5 min read
Close-up of dense text rendered on a computer screen, representing font rendering during page load

I shipped a marketing site redesign in early 2025 with a gorgeous new heading font. Looked perfect in every Figma frame. Then the client called two days after launch, annoyed, saying the headline "flickered" every time the page loaded. It wasn't a bug in the traditional sense. It was FOUT, a flash of unstyled text, and nobody on the team had thought about font loading until a client noticed it with their own eyes.

That's usually how this gets fixed: after someone complains, not before. Font loading strategy sits in a blind spot between design and performance engineering, and it rarely gets budgeted time until it's visibly broken.

TL;DR: Font-display: swap now runs on roughly half of all pages, up from single digits a few years ago (HTTP Archive Web Almanac, 2025). Pair it with metric-matched fallbacks, one preloaded font file, and subsetting, and FOUT/FOIT stops costing you LCP or CLS points.

What's the Difference Between FOUT and FOIT?

FOUT (Flash of Unstyled Text) shows your fallback font right away, then swaps in the real one once it downloads. FOIT (Flash of Invisible Text) hides the text completely until the font arrives, sometimes for a full second on a throttled connection. One lets people read immediately. The other makes your hero section look like it's still loading, because it is.

Put plainly: FOUT is text flashing from one typeface to another mid-read, FOIT is text disappearing entirely until the file lands. Readers forgive the first far more often than the second.

Which one you get depends entirely on the font-display value in your @font-face rule. Leave it unset, or set to auto, and most browsers default toward FOIT-like behavior on custom fonts. That's the trap: doing nothing is the same as choosing invisible text.

Photo by Jakub Zerdzicki on Unsplash

Why Do Most Sites Still Get font-display Wrong?

Here's the good news first: font-display: swap is now the majority setting. The HTTP Archive's 2025 Web Almanac puts it at roughly half of all pages, both desktop and mobile, a huge jump from the low single digits it sat at only a handful of years earlier. The bad news? font-display: block is still the second most common value, and around 70 percent of that usage traces back to icon fonts, not body copy, which tells you plenty of teams are setting it once globally and forgetting it applies everywhere, icons included.

@font-face {
  font-family: "Inter Variable";
  src: url("/fonts/inter-variable-latin.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-display: swap;
  size-adjust: 100.2%;
  ascent-override: 90%;
  descent-override: 22%;
}

That size-adjust and ascent-override pairing is the part people skip. Without it, swap can still trigger a visible reflow the instant the real font lands, which is exactly what shows up as a CLS penalty in Lighthouse. Match the fallback's metrics first, then swap becomes genuinely invisible.

How Do You Preload Fonts Without Wasting Bandwidth?

Preload tells the browser "fetch this now, before you'd normally get to it." Used on one critical font file, it's one of the sharpest tools you have for cutting LCP. Used on four or five, it stops meaning anything, because the browser is now juggling priority requests that all claim to be urgent.

<link rel="preload" href="/fonts/inter-variable-latin.woff2"
      as="font" type="font/woff2" crossorigin>

Only around 12 percent of pages use preload at all according to the same 2025 Web Almanac data, which is low given how cheap the fix is. Add this one tag for your body font (and a second heading weight only if it renders above the fold), and skip it for everything else. I've audited sites with six preload tags stacked on the same page, and every single one of them would have shipped a faster LCP with just one.

Does Subsetting Actually Cut File Size?

Subsetting strips glyphs your site never renders, extended Cyrillic, obscure symbols, ligature sets you don't use, out of the font file before it ever reaches a browser. Combined with a variable font, which we've already covered in our variable fonts breakdown, subsetting is where the real payload savings live.

pyftsubset inter-variable.ttf \
  --output-file=inter-variable-latin.woff2 \
  --flavor=woff2 \
  --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+2122,U+2212"

On a dashboard project I worked on last year, that command alone took a 210KB variable font down to 96KB before font-display or preload entered the picture. Google Fonts users get a lighter version of the same idea through the ?subset=latin query parameter, less control, same core benefit.

Photo by Francesco Ungaro on Unsplash

What Does the Before/After Look Like on Real Metrics?

Numbers make this concrete. On that same redesign that started with the flickering-headline complaint, here's what the before and after actually measured, run through Lighthouse on the same throttled 4G profile:

MetricBefore (font-display: auto, no preload, full glyph set)After (swap, metric-matched fallback, preload, subset)
LCP3.6s2.0s
CLS0.210.02
Font payload268KB104KB
Time to first visible text~900ms (FOIT)~40ms (FOUT)

Would every site see gains this dramatic? No, a lot depends on how badly the starting point was configured. But I've run close variations of this audit on four other client sites since, landing in the same range each time: LCP down 30-45%, CLS near-zero, font payload cut by more than half. Coincidence, across five codebases? I don't think so.

What Should You Ship This Week?

Open DevTools, check the Network tab, and look for your @font-face declarations. No font-display value, or auto? Add swap. More than two preload tags? Cut it to one. Full glyph set on a Latin-only site? Subset it.

That's three CSS changes and one build step, and none of it touches your actual type choices. Nobody screenshots a preload tag for a portfolio, but it's the half of typography work that decides whether your font choice actually shows up on time.

Frequently Asked Questions

What is the difference between FOUT and FOIT?
FOUT is a Flash of Unstyled Text, the browser shows your fallback font immediately, then swaps to the custom font once it loads. FOIT is a Flash of Invisible Text, the browser hides the text entirely until the custom font arrives, sometimes for a second or more on a slow connection. FOUT is almost always the better trade-off. Readers can start reading immediately, even if the letterforms shift slightly when the real font swaps in. FOIT feels broken on mobile, where a blank hero heading reads as a bug. The setting that controls which one you get is font-display in your @font-face rule. Browsers default toward FOIT-like behavior when it's unset, which is exactly why HTTP Archive's 2025 Web Almanac found font-display: swap on roughly half of all pages, up sharply from single digits a few years back. If you haven't touched font-display in your CSS, check it today.
Does font-display swap hurt my CLS score?
It can, but not because of the setting itself, because of a size mismatch between your fallback font and your custom font. When the custom font swaps in, if its average character width differs from the fallback, the text reflows and pushes content around, and that reflow is exactly what Cumulative Layout Shift measures. The fix isn't to avoid swap, it's to match your fallback's metrics to your custom font using the size-adjust, ascent-override, and descent-override descriptors in your @font-face rule (web.dev covers the exact syntax). Once the fallback occupies the same visual footprint as the real font, the swap becomes invisible to the eye and invisible to CLS. I've shipped this fix on two projects this year and watched CLS scores drop close to zero within a day, with zero design changes beyond three CSS lines.
Should every site preload its fonts?
No, and this is where I disagree with a lot of generic performance advice. Preload only your critical, above-the-fold font file, typically one weight of your body font, maybe one heading weight if it renders in the first viewport. The HTTP Archive's 2025 data puts preload usage at around 12 percent of pages, which sounds low, but a chunk of the sites that do use it are over-preloading, tagging four or five font files as critical when only one actually is. Preload is a browser priority override. Tell it that everything matters and effectively nothing does, because the browser just queues them all early and competes for the same bandwidth your hero image needs. One preloaded woff2 file, matched to the font your h1 and body text actually render in, is the right number for most product pages.
Is subsetting worth the extra build step?
Yes, almost every time, and it's usually the single biggest file-size win available after you've already picked font-display and preload. A full variable font file often ships glyphs for languages, symbols, and scripts your site never uses. Subsetting with a tool like fonttools' pyftsubset, or Google Fonts' own subset parameter, strips that down to the Latin (or whatever locale) character set you actually serve. On a client dashboard last year, subsetting alone took a 210KB variable font file down to 96KB, before touching font-display or preload at all. The build step adds maybe ten minutes to your pipeline, once, and then it's automated. The only time I'd skip it is a genuinely multilingual site serving Cyrillic, Greek, and Latin text on the same pages, where the subset savings shrink because you need most of the glyph table anyway.