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: width and height attributes on the header logo. Saved about 40% of the shift.
  • Fix #2: .header-nav rules moved into inline critical CSS in <head>. The header stopped jumping.
  • Fix #3: Inter Fallback font based on Arial with size-adjust:107% and ascent-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

MetricBeforeAfter
CLS Lighthouse mobile0.3770.002
CLS PageSpeed field data(n/a, new site)0.002 (after 3 days)
LCP mobile4.5 s2.4 s (alongside CLS - thanks to hero preload)
Lighthouse Performance~6093

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> with width and height attributes. 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-adjust from 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.

Related: how I work - no fluff, no 40-slide proposals.

Frequently asked questions

What is CLS and what is the normal value?
Cumulative Layout Shift is the sum of all unexpected layout shifts during page load. Green zone up to 0.1, yellow 0.1-0.25, red above 0.25. Both Google and Yandex have used CLS as a ranking factor since 2021. On my site it was 0.377 - meaning Lighthouse thought half the screen was jumping during load.
Why does the fallback font affect CLS?
While the browser downloads the woff2 of Inter, text renders in the fallback (Arial or system-ui). When Inter finishes loading, metrics change: letter width, line height. Text 'reflows', blocks beneath shift up or down. That is FOUT (Flash of Unstyled Text) - at 1-2 second load times on mobile, it produces a huge shift.
What is size-adjust in @font-face and how do you calculate it?
It is a CSS property that scales a fallback font to match the metrics of the target. The math: you compute x-height and average character widths for the target and the fallback, then pick a size-adjust value (80% to 120%) so the text occupies the same space. For the Inter ↔ Arial pair, the working value is 107% with an additional ascent-override:90%. You can use the fontkit calculator or take values from the Capsize repository.
What is critical CSS and why move the header into it?
Critical CSS is the styles needed to render the visible part of the page (above the fold). They are inlined directly in `<head>` so the browser does not wait for an external CSS file before the first paint. If header styles sit in an external file, the browser first paints content without the header, then loads CSS, and the header 'jumps' in from the top. That alone produces a 0.05-0.15 shift. Moving it to critical removes the step.
Does this work on a CMS or only on a custom build?
The approach is universal. WordPress has plugins like Critical CSS by NitroPack or Autoptimize that do the same automatically. On Tilda and similar builders you cannot reach into critical CSS without exporting to code. On Astro / Next.js it is built into the bundler. Logo dimensions in HTML work everywhere too: `width` and `height` attributes on `<img>` are supported by every browser since 2020.