Font Loading

How web font loading affects Core Web Vitals like CLS and LCP, font-display strategies, preloading fonts, and reducing layout shift from font swaps.

First published: Jul 2, 2026 · Last updated: Jul 17, 2026 · Advanced
demand #27 in Web Performance#257 in Technical SEO#347 on the site

Custom web fonts are external files that have to download before text can render in them, so the browser needs a plan for what to show in the meantime. That plan — set mainly by the CSS font-display property — moves two Core Web Vitals: LCP (when a render-blocking font delays your largest text) and CLS (when swapping the fallback for the web font shifts the layout). The two default behaviors, FOIT and FOUT, each have a failure mode. The reliable fixes: preload critical fonts, use font-display: optional or swap deliberately, match your fallback font's metrics, or skip custom fonts entirely. It's not a direct ranking factor — it feeds CLS/LCP, which are page experience signals.

TL;DR — Custom web fonts are render-affecting external resources. They hit LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good. when a render-blockingRender-blocking resources are CSS and synchronous JavaScript files a browser must download and process before it can paint any visible content. They sit on the critical rendering path and delay First Contentful Paint and Largest Contentful Paint. font delays your largest text element, and CLSCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good. when the fallback-to-web-font swap reflows the layout. The two default behaviors — FOIT (invisible until loaded) and FOUT (fallback then swap) — each fail differently. font-display is the primary lever (swap guarantees FOUT; optional gives a ~100ms window and then commits, killing the later swap). Preloading fixes late font discovery; preloadResource hints are <link> elements (or equivalent HTTP Link headers) that tell the browser to do network work — DNS lookups, connection setup, or fetching a resource — earlier than it would discover the need on its own. The main ones are dns-prefetch, preconnect, preload, modulepreload, and prefetch. + font-display: optional is the combo Google’s own engineering singled out for eliminating layout jank. But passing the LighthouseLighthouse is Google's free, open-source tool that audits a page under simulated lab conditions and scores it 0–100 across Performance, Accessibility, Best Practices, and SEO. It's lab data — useful for debugging, not a ranking signal. font-display check doesn’t fix CLSCumulative Layout Shift — a unitless score for unexpected visual movement, taken from the largest burst (session window) of layout shifts, not the lifetime sum. ≤0.1 is good. — that needs a metrically-matched fallback (or size-adjust/ascent-override overrides). None of this is a direct ranking factor; it feeds CLS/LCPLargest Contentful Paint — render time of the largest visible image or text block, relative to when the page started loading. ≤2.5 s (at the 75th percentile) is good., which are page experienceGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. signals.

Evidence for this claim The CSS Font Loading API exposes font loading state and control to documents. Scope: Current official or standards documentation. Confidence: high · Verified: MDN: CSS Font Loading API Evidence for this claim font-display controls how a font face is displayed while it downloads and when fallback is used. Scope: Current official or standards documentation. Confidence: high · Verified: MDN: font-display

Why fonts are a special case

Most performance problems are about bytes — an image is too big, a script blocks the main thread. Fonts are worse than that in one specific way: they’re an external resource and they change the metrics of your text. So a font can hurt you two different ways at once. It can delay when text paints (an LCP problem), and it can change the size and shape of that text after it’s already on screen (a CLS problem).

The two default browser behaviors are worth naming because the whole topic hangs off them:

  • FOIT (Flash of Invisible Text): text in the custom font is rendered invisible until the font loads or a timeout is hit — historically up to a ~3-second block in most browsers.
  • FOUT (Flash of Unstyled Text): the fallback font shows immediately, then gets swapped for the web font once it loads.

FOIT feels “cleaner” (no jump) but risks a renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. delay. FOUT feels faster (text now) but risks a layout shift on swap. Neither is automatically safe — that’s the core idea to hold onto.

It also helps to separate the pipeline into distinct stages, because success at one stage doesn’t guarantee success at the next. A @font-face rule only declares a face — the browser fetches the actual font file only once matching styled text on the page requires it (declaration and matching are separate steps from the fetch). That fetch is subject to CORS if the font is served cross-origin, so a stylesheet that loads cleanly doesn’t guarantee the font response itself carries the right cross-origin permission — a missing header there fails silently as a font that never renders. And a successful fetch still doesn’t guarantee a stable-looking page: cold-cache, warm-cache, and already-cached font states can produce different discovery and swap timing, so testing one repeat page load in DevTools doesn’t tell you what a first-time visitor actually sees.

How fonts affect LCP

If the largest visible element on your page is text — a big headline, a hero paragraph — then the font that text uses can become part of your LCP path. Per web.dev’s LCP guidance, the LCP resource of a page, if it has one, will be either an image or a web font. A render-blocking font that leaves that text invisible (FOIT) pushes your LCP out until the font arrives.

The fix here is straightforward and comes straight from Google. On web.dev’s Optimize Largest Contentful Paint guide:

“If you set a font-display value of anything other than auto or block, then text will always be visible during load, and LCP won’t be blocked on an additional network request.”web.dev

In other words: swap, fallback, or optional all keep text visible during load, so your LCP isn’t hostage to the font request.

How fonts affect CLS

CLS comes from the swap. Your fallback font and your web font almost never have identical character widths and line heights, so when the browser replaces one with the other, the text reflows — and everything below it moves. That’s a layout shift.

The counterintuitive part, and the thing most people miss: FOIT causes CLS too. Even invisible text is still laid out using the fallback font’s metrics, so the swap still moves things. From web.dev’s Optimize Cumulative Layout Shift guide:

“Both approaches can cause layout shifts. Even if the text is invisible, it’s still laid out using the fallback font, so when the web font loads, the text block and the surrounding content shift”web.dev

The same page frames the two behaviors plainly:

“The fallback font is swapped with the web font, incurring a Flash of Unstyled Text (FOUT). ‘Invisible’ text is displayed using the fallback font until a web font is available and the text is made visible (FOIT—flash of invisible text).”web.dev

I’ve said the same thing in plainer terms in my Ahrefs write-up on Cumulative Layout Shift: when a font loads or changes, you end up with a noticeable shift — a FOIT or a FOUT. It’s one of the most common real-world CLS causes, right alongside images without dimensions and content injected after load.

The font-display property

font-display is a @font-face descriptor that controls two windows: the block period (how long the browser hides text waiting for the font) and the swap period (how long, after the block period, it will still swap in the web font once it arrives). Per web.dev’s Best Practices for Fonts:

ValueBlock periodSwap period
autoBrowser-dependentBrowser-dependent
block2–3 secondsInfinite
swap0msInfinite
fallback100ms3 seconds
optional100msNone

MDN’s font-display reference, which Google’s own docs link to as the spec source, describes them tersely: swap gives an extremely small block period and an infinite swap period; optional gives an extremely small block period and no swap period. That “no swap period” is the whole point of optional — after the block period window closes, whatever font is in use stays, so there’s no later swap to shift the layout.

One caution on the numbers in that table: the CSS specification itself only defines block/swap period categories — short, extremely small, infinite, none — and leaves the exact duration to the browser. The millisecond/second figures above are web.dev’s documented Chromium behavior, not a cross-browser guarantee. Treat them as illustrative rather than a spec promise, and check current numbers for whichever browser/version you’re actually testing against.

Which value for which content? web.dev’s own advice is that these can be mixed: use swap for branding and other visually distinctive elements (where seeing the brand font matters and a brief shift is acceptable), and optional for body text (where CLS-sensitivity wins and you’d rather not shift). That’s a genuinely useful rule of thumb — don’t apply one value site-wide by reflex.

Preloading fonts

Font requests are discovered late. The browser doesn’t know it needs a font until it has parsed your CSS, matched a @font-face rule to an element on the page, and decided that element is visible. Only then does it start the download. A <link rel="preload"> short-circuits that:

<link rel="preload" href="/fonts/brand.woff2" as="font" type="font/woff2" crossorigin>

That tells the browser to start fetching the font immediately, in parallel with everything else, instead of waiting to discover it. Bing’s engineering team describes exactly this mechanism on their own search pages (more in the Quotes tab): the preload tags in the <head> kick off the font downloads right away, whereas without them the browser wouldn’t fetch the fonts until it had parsed the CSS and foundA 302 (\"Found\") is a temporary redirect: it forwards users to a new URL while telling search engines the original URL should stay in the index. It's a weak canonicalization signal, not the zero-equity dead end of SEO folklore. matching elements.

The strongest combination per Google’s own writeup is preload + font-display: optional. web.dev’s Prevent layout shifting and FOIT by preloading optional fonts calls combining <link rel="preload"> with font-display: optional the most effective way to guarantee no layout jank when renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. custom fonts, and notes that Chrome (from version 83) eliminated the layout shifting that used to happen when preloading optional fonts. The preload gives the font its best shot at arriving inside the ~100ms window; optional guarantees that if it doesn’t, you don’t pay for it with a shift.

When not to preload. Preloading isn’t free. web.dev warns that preload is highly effective at making fonts discoverable early, but it comes at the cost of taking browser resources away from loading other resources. Preload the one or two fonts you actually need above the fold — preloading every font weight you own can starve more important resources and make the page slower overall.

Make sure the preload actually gets reused. A preload only helps if the browser can match it to the real font request — the href, as="font", type, and crossorigin mode all have to line up with what the CSS @font-face rule ends up requesting, or you’ll get an “unused preload” warning and a wasted second download instead of a faster one. Preloading can also bypass unicode-range selection, pulling down a subset you didn’t actually need for that page. Confirm the match in DevTools’ Network panel: you want to see one request for that font, kicked off by the preload, not two.

Fixing the shift itself: fallback metric matching

Here’s the trap: passing the LighthouseLighthouse is Google's free, open-source tool that audits a page under simulated lab conditions and scores it 0–100 across Performance, Accessibility, Best Practices, and SEO. It's lab data — useful for debugging, not a ranking signal. font-display check does not fix CLS. Google’s Lighthouse docs are explicit that FOIT and FOUT have the same impact on CLS once the custom font replaces the temporary system font. So swap makes your text visible (good for LCP and for the “invisible text” audit) but leaves the swap shift in place.

To kill the shift when a swap does happen, you have to make the fallback font and the web font take up the same space. Two levers:

  1. Pick a metrically-similar system fallback in your font-family stack, rather than a single custom font name with nothing sensible behind it. The closer the fallback’s character widths and line height are to the web font, the smaller the shift.
  2. Override the web font’s metrics with size-adjust, ascent-override, descent-override, and line-gap-override on the @font-face rule, so the web font is forced to match the fallback’s box. web.dev lists these descriptors, and Smashing Magazine’s deep dive on CSS font descriptors walks through them in detail if you want to go all the way.

Each of those four descriptors adjusts a different metric, and current browser support varies by descriptor — check compatibility before leaning on any single one. Validate against the actual weights, styles, and scripts you ship, too: a fix confirmed on your regular-weight Latin text doesn’t guarantee bold, italic, or a non-Latin script behave the same way.

DebugBear’s Fixing Layout Shifts Caused by Web Fonts is a good end-to-end walkthrough of this: measure the impact, identify the problem font, apply font-display, choose a better fallback, then adjust font metrics.

Google Fonts vs. self-hosting

You don’t have to self-host to control font-display. The Google Fonts CSS2 API accepts a display query parameter — https://fonts.googleapis.com/css2?family=Roboto&display=swap sets font-display directly on the served @font-face rules. Addy Osmani’s post on shipping font-display to Google Fonts covers the change: previously the only way to specify font-display for Google Fonts was to self-host them, and this removed that need.

Self-hosting still wins on one thing: it eliminates the extra DNS lookup and connection to fonts.googleapis.com and fonts.gstatic.com. That’s a real latency cost, and one of the reasons a preconnect resource hintResource hints are <link> elements (or equivalent HTTP Link headers) that tell the browser to do network work — DNS lookups, connection setup, or fetching a resource — earlier than it would discover the need on its own. The main ones are dns-prefetch, preconnect, preload, modulepreload, and prefetch. to those origins helps if you do use Google Fonts. Weigh the connection overhead against the convenience — and don’t assume “it’s from Google, so it’s fine.” A copy-pasted Google Fonts embed is still a render-blocking stylesheet request plus a font request, and older embeds may not carry a display value at all.

There’s no universal winner between the two options — it comes down to which scripts and weights you actually ship, your caching setup, and the font’s license terms. Run the comparison against your own pages rather than defaulting to whichever one a tutorial recommended.

Variable fonts

A variable font packs many weights and styles (regular, bold, condensed, italic) into a single file with adjustable axes. That can be a net win — one request instead of six — if you actually use several of those variants. But a full variable font is a bigger single file than one static weight, so if you only ever render one weight, shipping the whole variable font can be slower, not faster. Subset it to the axes and characters you use, serve it as WOFF2 so it’s compressed, and check the font’s license — not every foundry permits subsetting or self-hosting.

Reducing the file in the first place

Everything above manages when the font loads. You can also shrink what loads. Font subsetting strips out glyphs you don’t use (an all-Latin site doesn’t need Cyrillic and CJK ranges), and WOFF2 is the well-compressed format to serve. A smaller font file is more likely to arrive inside optional’s window and less likely to block LCP — subsetting and compressionCompression (HTTP content encoding) shrinks text-based responses — HTML, CSS, JS, JSON, SVG, XML sitemaps — before they're sent over the network, using an algorithm like Gzip, Brotli, or Zstd, so the browser or crawler downloads fewer bytes. It's not a ranking factor, but it speeds up page loads and helps pages stay under crawler fetch limits. are the complementary lever to font-display and preload, not an alternative to them.

There’s no single “correct” subset or universal byte-savings percentage here, though — it depends on the scripts, characters, and weights your specific audience actually needs. A number you saw in someone else’s case study or benchmark isn’t a promise for your pages; measure your own font requests before and after.

Is this a ranking factor?

Be precise here. Font loadingFont loading is how a browser fetches, applies, and renders custom web fonts — and specifically what it shows while the font file downloads. Those choices, controlled mainly by the CSS font-display property, directly affect two Core Web Vitals: LCP and CLS. is not a direct ranking factor. What it affects is CLS and LCP, which are Core Web VitalsGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. and part of Google’s page experience signals. And page experience is a lightweight, tiebreaker-style signal — relevance and quality dominate; it helps decide between otherwise comparable results, not between a great page and a poor one. So the honest framing is: fix your font loading because it’s a genuine user-experience problem that happens to feed two ranking inputs — not because a font-display value will move you up the results on its own.

My own take

Across my Ahrefs CLS and LCP guides I’ve landed on the same priority order, and it still holds. If you can use a system font, do that — there’s nothing to load, so there are no delays or changes that cause a shift. If you have to use a custom font, the current best method for minimizing CLS is to combine <link rel="preload"> (grab the font as soon as possible) with font-display: optional (give it a small window to load); if it doesn’t make it in time, the page just shows a default font, and your custom font gets cached and shows up on subsequent loads. Preload, then optional, then — best of all — just don’t use a custom font. That’s the whole ladder.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.