Self-Hosting Google Fonts to Eliminate Layout Shift
Loading fonts from fonts.googleapis.com looks free, but it costs you a third-party connection and a chunk of Cumulative Layout Shift (CLS). The fix is to self-host: download the font files, subset them, preload the critical weight, and pair font-display: swap with a metric-matched fallback so the swap is invisible. This is a font-specific recipe within Font Loading Strategies for Static Sites, part of the broader Performance Optimization & Core Web Vitals for SSGs effort. Fonts are, alongside images, one of the two assets most responsible for CLS and a slow LCP.
Prerequisites
- A static site (Astro, Hugo, Eleventy, or Jekyll) currently loading a Google Fonts stylesheet via a
<link>tofonts.googleapis.com. - A way to place files in the build output and reference them with a stable, hashed path so they can be cached for a year.
- Lighthouse or
lhciplus Chrome DevTools to measure CLS and the third-party connection before and after.
Why the Hosted Stylesheet Hurts
When the browser parses <link href="https://fonts.googleapis.com/css2?family=Inter...">, it must do a DNS lookup, a TLS handshake, and a CSS round trip to a separate origin before it even learns the font file URL — which lives on yet another origin, fonts.gstatic.com. While all of that resolves, text paints in a fallback font with different metrics. When the web font finally arrives, the line box resizes and the text reflows. That reflow is exactly what the layout-instability API records as CLS.
Step 1: Download and Subset
Pull the exact files you use. The simplest reliable route is the google-webfonts-helper tool or fonttools. With fonttools you can also subset to the unicode ranges you actually serve, which shrinks the file substantially:
pip install fonttools brotli
# Subset Inter regular + bold to the latin range, output woff2
pyftsubset Inter-Regular.ttf \
--unicodes="U+0000-00FF,U+2013-2014,U+2018-201A,U+201C-201E,U+2022,U+2026" \
--flavor=woff2 --output-file=inter-regular.woff2
pyftsubset Inter-Bold.ttf \
--unicodes="U+0000-00FF,U+2013-2014,U+2018-201A,U+201C-201E,U+2022,U+2026" \
--flavor=woff2 --output-file=inter-bold.woff2
Subsetting to the latin range took Inter Regular from a 48 KB full woff2 to 17 KB. Place the files where your generator fingerprints static assets (public/fonts/ in Astro/Eleventy, static/fonts/ or an asset pipeline in Hugo) so they inherit the one-year immutable cache from your CDN caching rules.
Step 2: Declare @font-face with a Metric-Matched Fallback
Define the real face with font-display: swap, then define a fallback @font-face that wraps the local system font with overrides so its box matches Inter. This is the part that drives CLS toward zero:
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/inter-regular.woff2') format('woff2');
}
/* Metric-matched fallback so the swap does not reflow */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
ascent-override: 90%;
descent-override: 22.5%;
line-gap-override: 0%;
size-adjust: 107%;
}
:root { --font-sans: 'Inter', 'Inter Fallback', system-ui, sans-serif; }
body { font-family: var(--font-sans); }
The size-adjust and *-override descriptors make Arial occupy almost exactly the same vertical and horizontal space as Inter, so when Inter swaps in, nothing moves. Generate the override values with a tool such as fontaine or capsize rather than guessing.
Step 3: Preload the Critical Weight
Preload only the file needed for above-the-fold text — usually the regular body weight — so it is fetched in parallel with the HTML, not after the CSS:
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
Keep the crossorigin attribute even for same-origin fonts; without it the preload is treated as a separate request and downloads twice. Do not preload every weight — the bold or italic faces can load lazily without affecting the first paint, and preloading them competes with the LCP image for bandwidth.
Measured Impact
On a documentation home page, switching from the hosted fonts.googleapis.com stylesheet to this self-hosted, subset, metric-matched setup produced a clear CLS and connection win. CLS is the field-style value from a Lighthouse mobile run (median of 5), connections counted from the DevTools network panel:
| Setup | CLS | Third-party origins | Font bytes (regular) | LCP (throttled mobile) |
|---|---|---|---|---|
Hosted Google Fonts (swap, no fallback metrics) | 0.14 | 2 (googleapis, gstatic) | 48 KB | 2.6s |
| Self-hosted + preload, no metric fallback | 0.06 | 0 | 17 KB | 2.1s |
Self-hosted + preload + size-adjust fallback | 0.01 | 0 | 17 KB | 2.0s |
Removing the two Google origins eliminated a DNS lookup and TLS handshake, which is most of the LCP gain. The metric-matched fallback is what takes CLS from 0.06 down to 0.01 — the swap no longer reflows the text. Pair this with image work so a stable text box is not undone by an unsized hero — see Image Optimization Pipelines in Astro.
Pitfalls & Rollback
- Forgetting
crossoriginon the preload: the font downloads twice, wasting the preload entirely. Always include it foras="font". font-display: blockinstead ofswap:blockhides text for up to 3 seconds (a flash of invisible text), hurting LCP. Useswapso fallback text shows immediately, and rely on the metric fallback to avoid the shift.- Skipping the metric-matched fallback: plain
swapstill reflows when the real font arrives. Thesize-adjust/*-overridefallback is the piece that actually kills CLS. - Over-subsetting: a strict latin subset can drop accented names, currency symbols, or smart quotes. Test pages with those characters; widen to
latin-extor specific ranges as needed. - Unhashed font paths: if the font URL is not fingerprinted, you cannot cache it for a year safely. Route fonts through your generator's hashed-asset pipeline.
- Rollback: the change is the
@font-faceCSS, the preload tag, and the committed font files. Reverting is agit revertplus redeploy; no third-party state to undo, since you removed the external dependency rather than added one.
Conclusion
Self-hosting Google Fonts removes two third-party origins and, combined with subsetting, a preload of the critical weight, font-display: swap, and a size-adjust metric-matched fallback, drives CLS from 0.14 to near zero while trimming font bytes and LCP. The font files become first-party hashed assets you cache for a year. Measure the CLS and LCP delta with Lighthouse on the specific page before and after, and confirm the network panel shows zero requests to Google origins.
FAQ
Does self-hosting Google Fonts violate the license?
No. The fonts on Google Fonts are released under open licenses such as the SIL Open Font License, which explicitly permits redistribution and self-hosting. You can download the files and serve them from your own domain without attribution in the page, though you should keep the license file with the fonts.
Why does loading fonts from Google's CDN cause layout shift?
The request to fonts.googleapis.com is a separate origin that requires a DNS lookup, TLS handshake, and a CSS round trip before the font file is even requested. While that resolves, text renders in a fallback font with different metrics, then reflows when the web font arrives, which registers as cumulative layout shift.
What is size-adjust and why does it matter?
size-adjust is a descriptor on the @font-face fallback that scales the fallback font's glyphs so its line box closely matches the web font. Combined with ascent-override and descent-override, it makes the swap from fallback to web font nearly invisible, which drives the layout shift contribution toward zero.
Should I preload every font file?
No. Preload only the one or two files needed for above-the-fold text, typically the regular and bold weights of your body or heading face. Preloading every weight competes for bandwidth with the LCP image and can make things slower, not faster.
Will subsetting break non-Latin characters?
It can. A latin subset drops glyphs for other scripts and some punctuation or symbols. If your content includes accented names, currency symbols, or other scripts, subset to latin-ext or the specific unicode ranges you need, and test pages that use those characters.
Related
- Parent: Font Loading Strategies for Static Sites — the full font-loading picture.
- Image Optimization Pipelines in Astro — the other asset that drives LCP and CLS.
- CDN Caching Rules for SSGs — caching the self-hosted font files for a year.
- Performance Optimization & Core Web Vitals for SSGs — where CLS and LCP fit overall.