CLS 0.377 → 0.002 in a day: three rounds of critical CSS fixes
Layout shift 0.377 on a fresh site. By the same evening - 0.002. Three fixes: logo dimensions, header-nav in critical CSS, Inter Fallback with size-adjust:107%.
On March 22, 2026 I shipped a new website for the cleaning company where I run marketing as head of marketing. The first Lighthouse mobile report showed CLS 0.377 - red zone, ranking gets actively cut both in Google and in Yandex. By the same evening: 0.002. Three fixes, no rewrite.
The short version
- Start: CLS 0.377. After three critical CSS fixes: 0.002. One working day.
- Fix #1:
widthandheightattributes on the header logo. Saved about 40% of the shift.- Fix #2:
.header-navrules moved into inline critical CSS in<head>. The header stopped jumping.- Fix #3: Inter Fallback font based on Arial with
size-adjust:107%andascent-override:90%. FOUT stopped reflowing text on woff2 load.
What CLS is and why 0.377 is bad
Cumulative Layout Shift is a metric Google introduced in 2020 as part of Core Web Vitals. It counts how often and how strongly page content jumps during load. A jump is when a text block or image moves after the user can already see the page, and not because of their action. Green zone up to 0.1, red above 0.25.
Google and Yandex factor Core Web Vitals into the overall quality assessment of a site - not as a one-to-one ranking factor, but as a signal in the mix. A site with CLS 0.377 loses to neighbors in the same SERP, even if content and links are stronger.
My first measurement showed 0.377. That meant Lighthouse saw the page as literally jumping. Not surprising: I prioritized a fast launch over metric polish. Four days after release I sat down and closed all three causes in one evening.
Round 1: logo dimensions in HTML
The first thing Lighthouse flagged in Performance Insights was “Image elements do not have explicit width and height”. Specifically, the header logo. My header.php looked like this:
<a href="/" class="header__logo">
<img src="/logo.svg" alt="Company" />
</a>
The browser does not know the rendered size of the logo until it has actually downloaded it. And while it has not downloaded - reserves default space. When the logo loads, its real size ‘pushes’ neighboring blocks. The header lurches half a second, and the hero slides with it.
Elementary fix:
<a href="/" class="header__logo">
<img src="/logo.svg" alt="Company" width="180" height="44" />
</a>
The browser sees the attributes, computes aspect-ratio 180/44, reserves space immediately. When the logo loads, no shifts.
Same story with the hero image on the home page - <img> with no dimensions and no CSS reservation. I added width="1200" height="630" and, for safety, style="aspect-ratio: 1200/630". After these two fixes, CLS dropped from 0.377 to around 0.21. Yellow zone, but not yet green.
Lesson. Dimensions on every <img> are the cheapest CWV fix. Supported in every browser since 2020, zero risk. If you have an old site without them - this is the first thing to put in, before anything else.
Round 2: header-nav in critical CSS
After fix #1 the header still ‘grew in’ from the top for half a second. In the Chrome DevTools Performance tab I could see: between First Paint and First Contentful Paint the browser had not yet applied .header-nav styles, so the header rendered as a column of links - no flex, no spacing, no borders. Then, when the CSS file loaded, the header snapped into shape.
My <head> already had an inline critical CSS block - body, .hero, h1, basic typography. But there were no rules for the header. They sat in /css/main.css, loaded in a separate request and applied after.
I moved the header block into inline critical:
<style>
/* ...existing... */
/* CRITICAL: header */
.header { background: #fff; border-bottom: 1px solid #e8e8e8; padding: 14px 0; }
.header__inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; gap: 32px; }
.header__logo { display: inline-flex; }
.header-nav { display: flex; gap: 24px; align-items: center; }
.header-nav a { color: #1a1a1a; text-decoration: none; font-weight: 500; }
/* + mobile burger */
@media (max-width: 768px) { .header-nav { display: none; } .header__burger { display: block; } }
</style>
Same rules removed from main.css to avoid duplication. After the rebuild, CLS dropped from 0.21 to 0.05. Green zone.
Simple idea - everything that renders above the fold and is visible in the first frame should live in inline critical CSS. Then the browser paints the page in one pass, no repaints.
Round 3: Inter Fallback with size-adjust
CLS 0.05 is already fine, but I wanted near-zero. The remaining shift came from the font. I self-hosted Inter at /fonts/inter.woff2 and loaded it like this:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
body { font-family: 'Inter', Arial, sans-serif; }
What happens on load. First frame - the browser sees Inter is not in yet, renders text in Arial. Text takes Arial-metric space - different letter widths, different line height. When woff2 finishes loading (200-400 ms on 4G), the browser instantly re-renders text in Inter. Metrics change: a sentence that took two lines now takes one and a half. The block below shifts up by 28 pixels. CLS counts that as a shift.
Solution - a fallback font with the same metrics as the target. I took Arial as the base (present on every system, no download) and tuned it with size-adjust and ascent-override so its metrics match Inter.
Working combination:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
body { font-family: 'Inter', 'Inter Fallback', Arial, sans-serif; }
What it does. The browser tries Inter first - while it is not there, walks down the stack. Sees ‘Inter Fallback’ - essentially Arial with stretched metrics. Text occupies the same space Inter will after load. When real Inter arrives 300 ms later, the text just changes appearance but does not shift layout - metrics already match.
Where to get the values. I took them from the Capsize repository - there are ready-made tables for popular ‘target font + Arial’ pairs. For Inter with Arial the working set is size-adjust:107%, ascent-override:90%, descent-override:22%. If your base font is different, calculate with the Capsize calculator or fontkit.
After this fix CLS dropped to 0.002. Lighthouse mobile - green zone, the report field turned green. For sport I also ran PageSpeed Insights field data from Chrome - 0.002 there too after three days of accumulation.
What got measured after
| Metric | Before | After |
|---|---|---|
| CLS Lighthouse mobile | 0.377 | 0.002 |
| CLS PageSpeed field data | (n/a, new site) | 0.002 (after 3 days) |
| LCP mobile | 4.5 s | 2.4 s (alongside CLS - thanks to hero preload) |
| Lighthouse Performance | ~60 | 93 |
In parallel with the CLS work in round 3 I added <link rel="preload" as="image" href="/hero.webp" fetchpriority="high"> for the hero image - that pulled LCP down. Not strictly part of the CLS story, but the image followed.
If I were starting over - day one, not day four
The main takeaway. All three fixes could have been done at launch, before release. They do not require rewriting the site, they do not require a new stack, they take 3-4 hours over two cups of coffee.
I kept pushing them to ‘later’, and the site lived in the red zone for four days. On a new domain those four days could have cost me ranking: both Google and Yandex factor CWV into the first quality assessment of a new site. If the first measurement is red - catching up later takes time.
Day-one checklist for any new site:
- Every
<img>withwidthandheightattributes. Rule, not a suggestion. - Header and hero fully inline in critical CSS. No external CSS requests for above-the-fold styles.
- Self-hosted fonts with a fallback via
size-adjustfrom the system font. Do not leave Arial unadjusted - it does not match metrics with Inter, Roboto, or Open Sans. - Lighthouse pass before release. CLS > 0.1 - delay release until it is fixed.
The full case on 50 days of SEO in B2B cleaning - in the article ‘50 days of SEO in B2B cleaning’, where CLS is one item in a broader metrics picture.