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.
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.
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-displayTL;DR — A custom web font is a file the browser has to download before it can show your text in it. While it waits, it either hides the text or shows a backup font and swaps later — and either choice can slow down or visually jump your page. You control that behavior with one CSS line (
font-display), and you can speed the font up by preloading it. If you don’t need a custom font, a system font has nothing to load and nothing to shift.
What font loading is
When you use a custom font — a brand font, a Google Font, anything that isn’t already on the visitor’s device — the browser has to go fetch that font file before it can paint your text in it. That download takes time, and the browser has to decide what to show in the meantime.
It has two basic options, and both have annoying names:
- FOIT (Flash of Invisible Text): hide the text until the custom font arrives. The page looks blank where the text should be, then it pops in.
- FOUT (Flash of Unstyled Text): show a normal backup font right away, then swap to the custom font once it loads. You see text immediately, but it can visibly “jump” when the swap happens if the two fonts aren’t the same size.
Why it matters for SEO
Neither of those is free. FOIT can delay when your main text appears, which hurts Largest Contentful Paint (LCP). FOUT can cause a layout shift when the font swaps in, which hurts Cumulative Layout Shift (CLS). Both LCP and CLS are Core Web Vitals — real-user metrics Google uses as part of page experience. So font loading isn’t just a design detail; it’s a technical SEO lever.
The simple fixes
- Use a system font if you can. There’s nothing to download, so there’s no delay and no shift. This is the most reliable fix, full stop.
- If you need a custom font, set
font-display. Addingfont-display: swap(oroptional) to your font tells the browser not to leave text invisible while it waits. - Preload the important font. A
<link rel="preload">tag tells the browser to grab the font early instead of stumbling on it later.
Want the full version — the font-display value table, the preload + optional
combo, matching your fallback font’s metrics, and Google vs. self-hosting? Switch
to the Advanced tab.
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-displayTL;DR — Custom web fonts are render-affecting external resources. They hit LCP when a render-blocking font delays your largest text element, and CLS 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-displayis the primary lever (swapguarantees FOUT;optionalgives a ~100ms window and then commits, killing the later swap). Preloading fixes late font discovery; preload +font-display: optionalis the combo Google’s own engineering singled out for eliminating layout jank. But passing the Lighthouse font-display check doesn’t fix CLS — that needs a metrically-matched fallback (orsize-adjust/ascent-overrideoverrides). None of this is a direct ranking factor; it feeds CLS/LCP, which are page experience signals.
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 rendering 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-displayvalue of anything other thanautoorblock, 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:
| Value | Block period | Swap period |
|---|---|---|
auto | Browser-dependent | Browser-dependent |
block | 2–3 seconds | Infinite |
swap | 0ms | Infinite |
fallback | 100ms | 3 seconds |
optional | 100ms | None |
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
found 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 rendering 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 Lighthouse 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:
- Pick a metrically-similar system fallback in your
font-familystack, 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. - Override the web font’s metrics with
size-adjust,ascent-override,descent-override, andline-gap-overrideon the@font-facerule, 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 hint 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 compression 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 loading is not a direct ranking factor. What it affects is
CLS and LCP, which are Core Web Vitals 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.
AI summary
A condensed take on the Advanced version:
- Font loading = what the browser shows while a custom font downloads. It’s a special case because fonts are both an external resource and they change text metrics — so they can hurt two Core Web Vitals at once.
- LCP: a render-blocking font that hides your largest text element (FOIT) delays
LCP. Any
font-displayvalue other thanauto/blockkeeps text visible so LCP isn’t blocked on the font. - CLS: the fallback-to-web-font swap reflows the layout. FOIT causes CLS too — invisible text is still laid out in the fallback’s metrics, so the swap still shifts.
font-displayis the main lever:swap= FOUT (fast text, still swaps);optional= ~100ms window then commits, so no later swap and no swap-related shift. web.dev suggests mixing them:swapfor branding,optionalfor body.- Preload fixes late font discovery. Preload +
font-display: optionalis Google’s singled-out combo for eliminating layout jank (Chrome 83+). Don’t over-preload — it steals resources from other loads. - Passing the Lighthouse font-display audit ≠ fixing CLS. The swap shift is a
separate fix: a metrically-matched fallback, or
size-adjust/ascent-override/descent-override/line-gap-override. - Google Fonts can set
font-displayvia the&display=URL parameter (no self-hosting needed); self-hosting still removes the extra connection hop. - Not a direct ranking factor — it feeds CLS/LCP, which are page experience signals (a tiebreaker). Best fix of all: use a system font.
Official documentation
Primary-source documentation from Google and Bing.
- Best Practices for Fonts (web.dev) — the
font-displayvalue table (block/swap periods) and the mix-strategy advice. - Optimize Cumulative Layout Shift (web.dev) — the FOIT/FOUT explanation and why both cause layout shift.
- Optimize Largest Contentful Paint (web.dev) — when a web font is the LCP resource and how
font-displaykeeps text visible. - Prevent layout shifting and FOIT by preloading optional fonts (web.dev) — the preload +
font-display: optionalcombo and the Chrome 83 fix. - Optimize WebFont loading and rendering (web.dev) — broader font-loading performance guide.
- Learn: Optimize web fonts (web.dev) — the structured lesson version.
- Ensure text remains visible during webfont load (Chrome for Developers) — the Lighthouse font-display audit. Note: as of Lighthouse 13 this moved from a standalone audit into a broader “Font display insight” panel, so older screenshots and guides use the old naming.
- Google Fonts CSS2 API —
displayparameter — settingfont-displayvia URL without self-hosting. - MDN —
font-display— the CSS spec reference Google’s own docs link to for value definitions.
Bing / Microsoft
- Fast Front-End Performance for Microsoft Bing (Bing Search Quality Insights, Aug 2022) — documents font preloading as a production technique on Bing’s own results pages. Note: this post confirms the preload mechanism only; it doesn’t cover
font-display, FOIT/FOUT, CLS, or LCP by name.
Quotes from the source
On-the-record statements from Google’s Chrome/web.dev docs and Bing engineering. Font
loading lives in the rendering/performance spec area, so the primary sources here are
the Chrome and web.dev teams (not Search Relations) — there is no on-record Search
team quote calling font-display a ranking topic, and I’m not going to invent one.
Google — web.dev (Chrome team)
- “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, Optimize Cumulative Layout Shift. Jump to quote
- “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, Optimize Cumulative Layout Shift. Jump to quote
- “If you set a
font-displayvalue of anything other thanautoorblock, then text will always be visible during load, and LCP won’t be blocked on an additional network request.” — web.dev, Optimize Largest Contentful Paint. Jump to quote
Bing — preloading fonts in production
- Bing’s engineering team describes preload tags in the
<head>that tell the browser to start downloading its custom web fonts immediately; without those tags, the browser wouldn’t fetch the fonts until it had parsed the CSS and encountered matching elements — so preloading makes them available in time for text rendering. Read the post
#:~:text= deep links jump straight to the quoted passage on the live page. Font-loading checklist
A quick pass to make sure fonts aren’t quietly wrecking your Core Web Vitals:
- Every custom
@font-face(or Google Fonts URL) sets a deliberatefont-displayvalue — not the browser default. - Body text uses
font-display: optional(or a metrically-matchedswap) to avoid a swap-driven layout shift. - The one or two fonts used above the fold are preloaded with
<link rel="preload" as="font" type="font/woff2" crossorigin>. - You’re not preloading every weight — only what’s needed for the initial render.
- The preload’s
href,as="font",type, andcrossoriginmatch the real@font-facerequest (checked in the Network panel) — no unused preload, no duplicate download. - Your
font-familystack names a sensible system fallback, not just the custom font. - If a swap still shifts, fallback metrics are matched (
size-adjust,ascent-override,descent-override,line-gap-override). - Fonts are served as WOFF2 and subset to the characters/weights you actually use.
- Google Fonts embeds include a
displayparameter and apreconnecttofonts.gstatic.com. - Variable fonts are only used when you render multiple variants — otherwise a single static weight may be lighter.
- You’ve confirmed with PageSpeed Insights / Lighthouse that fonts aren’t the LCP element or a CLS culprit.
The mental models
1. Fonts hit two vitals through two mechanisms.
LCP: a render-blocking font hides your largest text element until it loads. CLS: the
fallback→web-font swap changes text metrics and reflows. Fix them as two separate
problems, because one fix (swap) helps LCP but leaves CLS untouched.
2. FOIT vs. FOUT — both fail, differently. FOIT (invisible until loaded) risks a rendering delay → LCP. FOUT (fallback then swap) risks a layout shift → CLS. There is no “safe default”; you have to choose and mitigate.
3. The font-display decision.
optional = a ~100ms window, then commit with no later swap (best for CLS-sensitive
body text). swap = show text now, guarantee the brand font eventually (best for
branding, accept a small shift). Mix them per element rather than one value
everywhere.
4. Discovery vs. render.
Preload fixes discovery (the browser learns about the font early). font-display
fixes render behavior (what shows while it loads). Fallback-metric matching fixes
the shift itself. They’re three different levers — you often want all three.
5. The good / better / best ladder.
Good: preload your fonts (better still, same-origin, to drop the extra connection).
Better: font-display: optional, paired with preload, so a slow font just shows a
default and caches for next time. Best: use a system font — nothing loads, so no
delay and no shift.
Font loading — cheat sheet
font-display values (block / swap periods, per web.dev):
| Value | Block | Swap | Good for |
|---|---|---|---|
auto | Browser-dependent | Browser-dependent | Nothing — it’s the unmanaged default |
block | 2–3s | Infinite | Rarely — causes FOIT, risks LCP |
swap | 0ms | Infinite | Branding/headlines (fast text, but swaps → can shift) |
fallback | 100ms | 3s | A compromise: brief block, limited swap window |
optional | 100ms | None | Body text (no swap → no swap-driven CLS) |
Figures above are web.dev’s documented Chromium behavior. The CSS spec itself only defines block/swap period categories (short, extremely small, infinite, none) and leaves exact timing to the browser — verify current numbers for the browser you’re testing.
The two failure modes
- FOIT = invisible until loaded → risks delaying LCP.
- FOUT = fallback then swap → risks a layout shift → CLS.
- Both cause CLS once the swap happens; only fallback-metric matching or
optionalprevents it.
The fixes, in priority order
- Use a system font (nothing loads).
- Preload the critical font (
as="font" type="font/woff2" crossorigin). - Set
font-display(optionalfor body,swapfor branding). - Match the fallback’s metrics (
size-adjust,ascent-override,descent-override,line-gap-override). - Subset + WOFF2 to shrink the file itself.
Google Fonts fast facts
- Add
&display=swap(oroptional/fallback) to the CSS2 URL to setfont-displaywithout self-hosting. - Still a render-blocking stylesheet + a separate font request — add
preconnecttofonts.gstatic.com. - Self-hosting removes the extra DNS/connection hop.
Diagnose
- PageSpeed Insights / Lighthouse “Font display insight” (was the standalone “Ensure text remains visible during webfont load” audit before Lighthouse 13).
- Chrome DevTools Performance + Network panels — see when fonts load relative to first paint and shifts.
Font-loading myths and mistakes
Each of these is a real, common audit finding — with why it’s wrong and what to do instead.
“font-display: swap fully solves my CLS problem.”
Why it’s wrong: swap fixes FOIT (text is visible immediately, good for LCP and
the Lighthouse audit) but it guarantees FOUT — and if the fallback and web font
differ in size, the swap still shifts the layout. Google’s Lighthouse docs note FOIT
and FOUT have the same impact on CLS once the swap happens.
Do instead: pair it with a metrically-matched fallback (or use optional), so the
swap doesn’t move anything.
“Preloading a font always speeds up my page.” Why it’s wrong: web.dev is explicit that preload comes at the cost of taking browser resources away from loading other resources. Preloading fonts you don’t need above the fold delays more important work. Do instead: preload only the one or two fonts needed for the initial render.
“font-display: optional means the custom font never loads.”
Why it’s wrong: it loads and is cached — it’s just not used on that specific page
load if it misses the ~100ms window. Once cached, later page views typically show it
immediately.
Do instead: use optional for CLS-sensitive body text without worrying that
returning visitors miss the brand font.
“Any font-display value fixes the Lighthouse ‘invisible text’ warning, so I’m
done.”
Why it’s wrong: passing that audit is necessary but not sufficient — the swap
layout shift is a separate, related-but-distinct problem.
Do instead: after clearing the audit, check CLS and fix the fallback metrics if the
swap still shifts.
“Google Fonts are auto-optimized and can’t cause CWV problems.”
Why it’s wrong: a copy-pasted Google Fonts embed is still a render-blocking
stylesheet request plus a separate font request unless display and
preconnect/preload are configured. “It’s from Google so it’s fine” is a common
real-world audit miss.
Do instead: add &display=, preconnect to fonts.gstatic.com, and preload the
critical weight.
“Font loading is a UX-only concern with no SEO relevance.” Why it’s wrong: CLS and LCP are Core Web Vitals and part of Google’s page experience signals. Font choices sit directly in a technical SEO’s remit. Do instead: treat font loading as part of your CWV work — while keeping it in proportion (page experience is a tiebreaker, not a primary factor).
Find the fonts loading on a page
A quick DevTools Console snippet to list every font face the browser actually loaded, and whether each is done — useful for spotting a heavy or slow font.
Chrome DevTools Console
// List loaded font faces and their status
[...document.fonts].map(f => ({
family: f.family,
weight: f.weight,
style: f.style,
status: f.status, // "loaded", "loading", "unloaded", "error"
display: f.display, // the font-display value in effect
}));
// When did fonts finish loading? (relative to navigation start)
document.fonts.ready.then(() =>
console.log("All fonts ready at", performance.now().toFixed(0), "ms")
);Watch for font-driven layout shifts
Paste this in the Console to log CLS entries as they happen, including the elements that moved — handy for confirming a font swap is the culprit.
Chrome DevTools Console
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log("Layout shift", entry.value.toFixed(4),
entry.sources?.map(s => s.node));
}
}
}).observe({ type: "layout-shift", buffered: true });Check that a font file is WOFF2 and reasonably sized
Fonts should be WOFF2 and subset. Point these at a font URL to check its size and type.
macOS / Linux
curl -sI https://example.com/fonts/brand.woff2 \
| grep -iE "content-type|content-length"Windows / PowerShell
$r = Invoke-WebRequest -Method Head https://example.com/fonts/brand.woff2
$r.Headers["Content-Type"]; $r.Headers["Content-Length"]A large content-length (well into the hundreds of KB for a single weight) is a
subsetting/compression opportunity.
Which font-loading strategy fits the page?
How should the critical text font load?
Prove a font-loading change worked
Critical-font preload test
Test to run: reload with cache disabled and inspect the font request’s initiator and start time in the Network panel. Expected result: the intended critical font starts early and is reused by CSS without a duplicate download. Failure interpretation: the preload attributes do not match the CSS request or the font is not actually critical. Monitoring window: immediate. Rollback trigger: duplicate requests, console errors, or contention that delays more important resources.
Font-swap stability test
Test to run: throttle the load while recording the Performance panel Layout Shifts track. Expected result: fallback text remains visible and the final font does not create a measurable shift. Failure interpretation: fallback metrics still differ or the swap happens too late. Monitoring window: immediate across supported breakpoints. Rollback trigger: hidden text, clipped text, or worse CLS.
Field-vitals test
Test to run: compare post-release LCP and CLS for the changed template with the prior RUM baseline. Expected result: the targeted metric improves without degrading the other. Failure interpretation: the font was not the bottleneck or the strategy traded loading speed for instability. Monitoring window: RUM as traffic arrives; CrUX over its rolling window. Rollback trigger: either field metric worsens consistently.
Resources worth your time
My related writing
- What Is Cumulative Layout Shift (CLS) & How To Improve It (Ahrefs) — my CLS guide, including the preload +
font-display: optionalfix for font-driven shifts. - What Is Largest Contentful Paint (LCP) & How To Improve It (Ahrefs) — the good/better/best font ladder for LCP.
- What Are Core Web Vitals (CWVs) & How To Improve Them (Ahrefs) — where CLS and LCP fit in the bigger CWV picture.
- Patrick Stox on the Ahrefs blog — the rest of my technical SEO writing.
From around the industry
- Fixing Layout Shifts Caused by Web Fonts (DebugBear, Umar Hansa) — end-to-end: measure → find the font →
font-display→ better fallback → adjust metrics. - Ensure text remains visible during webfont load (DebugBear) — the Lighthouse-audit-specific troubleshooting angle.
- How to avoid layout shifts caused by web fonts (Simon Hearne) — a practitioner-level deep dive.
- FOUT, FOIT, FOFT (CSS-Tricks) — the canonical explainer for the terminology.
- A New Way To Reduce Font Loading Impact: CSS Font Descriptors (Smashing Magazine) — the
size-adjust/ascent-override/descent-override/line-gap-overridetechnique in depth. - How to Reduce Layout Reflow When Using Web Fonts (Material Design) — a practical CSS-descriptor walkthrough from Google’s Material team.
- We shipped font-display to Google Fonts (Addy Osmani) — the
displayURL parameter and why it removed the need to self-host forfont-displaycontrol.
Test yourself: Font Loading
Five quick questions on how web fonts affect Core Web Vitals. Pick an answer for each, then check.
Font Loading
Font 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.
Related: Core Web Vitals, Resource Hints
Font Loading
Font loading refers to the sequence of events between a page request and the point where text is stable and visible in its final font. Because most web fonts are external files (WOFF2 and friends) that must be downloaded before they can be used, the browser has to decide what to show while the font is loading — invisible text, a fallback system font, or nothing different at all.
That decision is controlled primarily by the CSS font-display property, and it directly affects two Core Web Vitals. It touches Largest Contentful Paint (LCP) when text is the page’s largest visible element and a render-blocking font delays it, and Cumulative Layout Shift (CLS) when the fallback font’s shape or size differs from the web font and the text (plus everything around it) reflows when the swap happens.
Two named behaviors sit at the center of the topic. FOIT (Flash of Invisible Text) hides text using the custom font until the font loads or a timeout is hit. FOUT (Flash of Unstyled Text) shows the fallback font immediately, then swaps to the web font once it arrives — visible right away, but able to shift the layout on swap. Neither is automatically safe: FOIT risks a rendering delay that hurts LCP, and FOUT risks a layout shift that hurts CLS.
Font loading isn’t a direct ranking factor on its own. It matters for SEO because it feeds CLS and LCP, which are part of Google’s page experience signals — so the levers here (font-display, preloading, and matching your fallback font’s metrics) are squarely within a technical SEO’s remit, not just a front-end concern.
Related: Core Web Vitals, Resource Hints
Build-time retrieval analysis plus live signals for this exact article. The automatic chunk report includes a deterministic readiness score and is ready without a model download.
Search Console
sampleGA4 traffic (28d)
sampleCloudflare traffic (7d)
sampledCrUX field data (28d, phone)
sampleGoogle NLP entities
localChangelog
Updated Jul 17, 2026.
Editorial summary and recorded change details.Summary
Corrected the font-display timing table to be explicit that exact ms/s figures are Chromium-documented behavior, not a cross-browser spec guarantee, and added the discovery/CORS/cache-state pipeline, preload request-matching, metric-override support caveats, and a decision-factors framing for Google Fonts vs. self-hosting and subsetting (no universal savings percentage).
Change details
-
Added a caveat to the font-display block/swap period table: the CSS spec only defines period categories (short/extremely small/infinite/none); the millisecond and second figures are web.dev's documented Chromium behavior, not a universal cross-browser guarantee.
-
Added a pipeline explanation (declaration/matching, CORS-gated fetch, cache-state variance) to 'Why fonts are a special case', and a preload request-matching checklist item (href/as/type/crossorigin must match the real @font-face request) to the Preloading section and checklist lens.
-
Softened Google Fonts vs. self-hosting, variable fonts, and subsetting sections to a measured decision framing (scripts/weights/license/caching) instead of implying a universal winner or savings percentage; added a browser-support caveat to the four metric-override descriptors.
Full comparison unavailable — no prior snapshot was archived for this revision.