If your Lighthouse report keeps flagging Cumulative Layout Shift issues and you’ve already optimized your images and ad slots, chances are your web fonts are the silent culprit. At GraphiteOne CAD, we recently worked through this exact problem on one of our marketing pages and brought CLS down from 0.34 to 0.02. In this guide, we walk through the precise steps, code, and before/after numbers so you can do the same.
Why Web Fonts Cause Cumulative Layout Shift
When a browser renders a page, it needs to know what font to use for each text block. If your custom font (say, Inter or Roboto) hasn’t downloaded yet, the browser has two choices:
- FOIT (Flash of Invisible Text): hide text until the font loads.
- FOUT (Flash of Unstyled Text): show a fallback font, then swap.
The swap from fallback to custom font is where layout shift happens. The fallback font (Arial, system-ui, Times) has different character widths and line heights than your custom font. When the swap occurs, every paragraph reflows, pushing buttons, images, and CTAs down the page. That reflow is what Google measures as CLS.

Step 1: Diagnose the Problem with Lighthouse and DevTools
Before fixing anything, confirm web fonts are actually the cause.
Run Lighthouse in Chrome DevTools
- Open your page in Chrome Incognito mode.
- Open DevTools (F12) and go to the Lighthouse tab.
- Run a Mobile performance audit.
- Look at the Avoid large layout shifts diagnostic.
Use the Performance Panel
Record a page load, then look for the purple Layout Shift bars in the timeline. Hover over them and check the affected node. If it’s a text element timed near a font request, you have your answer.
Our test page baseline looked like this:
| Metric | Before Optimization |
|---|---|
| CLS | 0.34 |
| Performance Score | 62 |
| LCP | 2.9s |
Step 2: Apply font-display: swap (or optional) Correctly
Most developers use font-display: swap, but it’s not always the right choice. Here’s the breakdown:
| Value | Behavior | CLS Impact |
|---|---|---|
| auto | Browser decides (usually FOIT) | Low CLS, bad UX |
| swap | Fallback shown, swaps when ready | High CLS without size-adjust |
| optional | Uses custom font only if ready in ~100ms | Zero CLS |
For most production sites, swap combined with size-adjust descriptors is the winning combo. Use optional only when font fidelity is not critical.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
font-weight: 400;
}
Step 3: Preload Critical Web Fonts
Preloading tells the browser to fetch your font early in the critical path, reducing the time the fallback is shown and therefore the magnitude of the shift.
Add this to your <head>, ideally before any CSS links:
<link rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin>
Three things to remember:
- Only preload fonts used above the fold. Preloading every variant hurts more than it helps.
- The
crossoriginattribute is mandatory for fonts, even when self-hosted. - Use the modern woff2 format. It’s about 30% smaller than woff.

Step 4: The Real Game Changer – size-adjust, ascent-override, and descent-override
This is where most tutorials stop, but it’s where the biggest wins happen. CSS descriptors let you reshape the fallback font’s metrics to match your custom font almost perfectly. When the swap happens, characters occupy the same vertical space, and CLS drops to near zero.
The Technique
Create a fallback @font-face block that wraps a system font and adjusts its metrics:
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
size-adjust: 107.4%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter-fallback', sans-serif;
}
How to Calculate the Right Values
Use the Fontaine library or the free online tool Fallback Font Generator by Malte Ubl. Both compute the exact override percentages by comparing the metrics of your custom font to common fallbacks like Arial, Times, or Georgia.
If you’re using Next.js 14+, the built-in next/font module does this automatically. For Nuxt projects, the @nuxt/fonts module handles it as well.
Step 5: Self-Host Your Fonts When Possible
Loading fonts from Google Fonts adds a DNS lookup, TLS handshake, and a CSS round trip before the font request even starts. Self-hosting cuts the font’s time-to-byte significantly and reduces the window where the fallback is visible.
- Download the woff2 files from google-webfonts-helper.
- Place them in
/public/fonts/. - Serve with
Cache-Control: public, max-age=31536000, immutable. - Preload only the variants used above the fold.
Before and After: The Lighthouse Numbers
Here’s the impact on our actual page after applying every technique in this guide:
| Metric | Before | After |
|---|---|---|
| CLS | 0.34 | 0.02 |
| Performance Score | 62 | 96 |
| LCP | 2.9s | 1.4s |
| FCP | 1.8s | 0.9s |

Common Mistakes to Avoid
- Preloading every font weight. This blocks the LCP image and makes things worse.
- Forgetting the crossorigin attribute on preload links. The browser will fetch the font twice.
- Using @import in CSS. It creates a render-blocking chain. Use
<link>instead. - Relying on font-display: block. It’s the worst case for UX and provides no real CLS benefit.
- Ignoring variable fonts. A single variable font file often replaces 4-6 static files and reduces the number of font swaps.
Quick Checklist Before You Deploy
- Self-host critical fonts in woff2 format.
- Add
<link rel="preload">for above-the-fold fonts. - Use
font-display: swapin your @font-face rules. - Define a fallback font-face with
size-adjustand override descriptors. - Stack your custom and fallback families in the CSS font-family declaration.
- Re-run Lighthouse and confirm CLS is below 0.1.
FAQ
What is a good Cumulative Layout Shift score?
Google considers a CLS score of 0.1 or less as good, between 0.1 and 0.25 as needs improvement, and above 0.25 as poor. For competitive SEO in 2026, aim for under 0.05.
Does font-display: swap always cause CLS?
Yes, by default, because the fallback and custom font have different metrics. The fix is not to abandon swap, but to pair it with size-adjust and override descriptors so both fonts render in the same space.
Should I use Google Fonts or self-host?
Self-host for the best CLS and LCP performance. Google Fonts adds an additional origin connection that delays the font fetch, even with preconnect hints.
Do I need to preload every font?
No. Only preload fonts used in the initial viewport. Preloading low-priority fonts steals bandwidth from your LCP image or critical CSS.
Will fixing CLS improve my SEO?
Yes. Core Web Vitals, including CLS, are confirmed Google ranking signals. A poor CLS score can suppress rankings even when content and backlinks are strong.
What about CLS on single-page applications?
SPAs face additional challenges because route changes can also trigger font loads. The same techniques apply, but make sure your font preload is in the root HTML document, not injected after hydration.
Fixing CLS caused by web fonts is one of the highest-ROI performance tasks you can do. The combination of preloading, font-display: swap, and properly calibrated fallback metrics typically takes less than an hour to implement and can transform a failing Core Web Vitals report into a passing one.
