Optimize CLS: concrete diagnostic and fixes that actually work

Your site can be “fast” on the stopwatch and still feel awful to use. The interface jumps right when the user wants to read, click or scroll. That’s exactly what CLS (Cumulative Layout Shift) measures: the visual stability of a page, the amount of unexpected shifts during its lifetime.

CLS is one of the technical pillars of SEO - for the bigger picture, see the 10 key points of SEO.

CLS Google is one of the three Core Web Vitals (with LCP and INP) used since 2021 as a ranking signal. Plainly: a bad CLS is a direct CLS SEO risk, dragging down your ranking on competitive queries and chasing users away before they even get to read your content.

The goal: aim for a CLS less than or equal to 0.1. Above 0.25 is considered poor.

References:


Understanding CLS (without getting lost)

CLS is computed from layout shifts: a shift happens when a visible element changes position between two frames without user action. A shift’s score depends on the portion of the viewport affected and the move distance.

Final CLS keeps the biggest “burst” of shifts over the page’s lifetime, with a session-window logic. Reference doc: https://web.dev/articles/cls

Thresholds

  • Less than or equal to 0.1: good
  • Between 0.1 and 0.25: needs improvement
  • Above 0.25: poor

Source: https://web.dev/articles/defining-core-web-vitals-thresholds

The detail that explains a lot of the “mysteries”

CLS is evaluated using session windows (max 5 seconds, with a 1-second gap). This avoids penalizing a page kept open very long, but a big late shift can count heavily if it lands inside a dense window.

Source: https://web.dev/blog/evolving-cls


CLS and SEO: why Google penalizes a bad score

CLS Google isn’t a cosmetic metric: it’s one of the Page Experience signals used by the algorithm since the Core Web Vitals rollout (June 2021, extended to desktop in 2022). Concretely:

  • A page with CLS above 0.25 is judged “Poor” in Search Console and loses weighting on queries where the competition is technically clean.
  • On competitive queries (e-commerce, lead, comparisons), where several results are equal on content, CLS SEO becomes a tiebreaker.
  • The business impact is twofold: less SERP visibility, and a higher bounce rate from the visitors who do land (an interface that jumps right when you want to click = immediate friction).

Bottom line: your SEO action plan should treat CLS like you treat internal linking or the title tag: a fundamental, not a late-stage optimization. To put it in the broader picture, see the 10 key points of SEO and the state of SEO in 2025.

Source: https://developers.google.com/search/docs/appearance/core-web-vitals


Lab vs Field: why Lighthouse sometimes “lies” to you

When you work on CLS, trap #1 is comparing metrics that don’t tell the same story.

Field (real): Search Console / CrUX

The Core Web Vitals report in Search Console relies on real user data (CrUX), aggregated by URL groups.

Source: https://support.google.com/webmasters/answer/9205520?hl=en

The upside: it’s the actual user reality (networks, devices, third-party scripts, AB tests). The limit: it doesn’t tell you which element shifts. From a business angle, a landing page that jumps kills SEA ROI just as much as SEO - see SEO or SEA in 2026.

Lab (simulation): Lighthouse / PageSpeed

Lighthouse is great to reproduce a behaviour, see where things move, test a fix quickly.

But Lighthouse can miss post-load shifts (chat widget, late ads, consent banner), device/network-specific behaviour, and some SPA scenarios (internal navigation, hydration).

Practical rule: if Search Console says “poor” but Lighthouse says “ok”, you very likely have post-load CLS or a shift not reproduced in lab.


Debug method (Chrome DevTools)

The goal isn’t “improve CLS” through voodoo, it’s to identify the element responsible.

Step 1: reproduce on a problematic URL

Pick a URL listed “Poor” in Search Console. Test on a clean profile (private window, empty cache) and on mobile if that’s where it breaks. Trigger the real actions: accept/decline cookies, scroll, open the menu, wait for the widgets.

Step 2: record a Performance trace

In Chrome, open DevTools, go to the Performance tab, hit record, reload the page and wait until everything renders (including third-party scripts). Stop the recording.

Find the Layout Shifts track and click an event to see the highlighted areas (the elements that move).

Guide: https://web.dev/articles/debug-layout-shifts

Step 3: classify the shift

Once you have the element, ask yourself:

  • Unreserved space? (image, iframe, ad slot, missing skeleton)
  • Font swap? (fallback to final font)
  • Injection above? (cookie banner, promo, notification, sticky bar)
  • Layout animation? (top/left/height/width)
  • Hydration/JS? (SPA, conditional rendering, AB testing)

This classification tells you what to fix first.


The #1 causes in production

The CLS killers we see in 80% of audits:

  1. Images and videos without dimensions: the browser doesn’t know how much space to reserve.
  2. Ads, embeds and iframes injected without a placeholder.
  3. Cookie or promo banners inserted into the flow above the content.
  4. Webfonts with different metrics that cause a visible reflow.
  5. Animations that modify layout instead of using transform.
  6. Third-party scripts that add DOM on the fly (chat, AB tests, tags).

Recommended reading: https://web.dev/articles/optimize-cls


Fixes (ready-to-paste snippets)

1) Reserve space for images and videos

This is usually the best return on investment.

Set width and height on images. Even in responsive, the browser derives the ratio:

<img
  src="/img/hero.jpg"
  width="1200"
  height="675"
  alt="Product overview"
  loading="lazy"
/>

Modern alternative with aspect-ratio, useful for wrappers and components:

.hero-media {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
}

Watch out: if your srcset changes ratio between breakpoints, you can reintroduce CLS. In that case, use separate containers (one per ratio) with reserved space.


2) Stabilize iframes and embeds (YouTube, Maps, socials)

An embed needs a stable box:

<div class="embed">
  <iframe
    src="https://www.youtube.com/embed/xxxx"
    title="YouTube video"
    loading="lazy"
    allowfullscreen
  ></iframe>
</div>
.embed {
  aspect-ratio: 16 / 9;
  width: 100%;
}

.embed iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

The layout doesn’t move while the iframe loads.


3) Ads: placeholder without surprise collapse

Ads hurt CLS because they load late and change size.

Minimum fix, reserve a height:

.ad-slot {
  min-height: 250px; /* adapt to your actual formats */
}

Reserve space (min-height, ratio, skeleton) for late-arriving content.

Doc: https://web.dev/articles/optimize-cls

The classic anti-pattern: an ad inserted above the content after initial render. If you have to put it there, reserve its slot from the first paint, even if the ad arrives later.


Two clean strategies:

Overlay (doesn’t push content): position: fixed, no impact on flow, near-zero CLS.

Reserve space from the start: you allocate the banner’s height in the initial HTML or via a skeleton.

What to avoid: a banner that gets inserted into the DOM above the content after the fact.


5) Fonts: reduce typographic shifts

Text can “reflow” when the final font arrives.

Preload critical fonts:

<link rel="preload" href="/fonts/brand.woff2" as="font" type="font/woff2" crossorigin>

Pick font-display wisely. swap is fast but can cause reflow. optional can reduce late swaps (and thus CLS):

@font-face {
  font-family: "Brand";
  src: url("/fonts/brand.woff2") format("woff2");
  font-display: optional;
}

Pick a fallback in the same family (serif vs sans-serif). For demanding cases, look into size-adjust to bring metrics closer.

Doc: https://web.dev/articles/optimize-cls


6) Animations: transform and opacity only

If you animate top/left/height/width, you trigger layout recalcs.

Prefer:

.badge {
  transition: transform .2s ease, opacity .2s ease;
}

.badge.is-open {
  transform: translateY(0);
  opacity: 1;
}

7) SPA and frameworks: hydration, conditional rendering, layout jank

Typical cases: a client-rendered component replaces an SSR placeholder that’s too small, a “reco” block injects when the API responds, a sticky bar appears once JS is ready.

Fix: render a stable box from the start. Same-height skeleton, container with min-height, per-breakpoint reservations.

The win is often big and more durable than micro-optimizations.


8) Bonus: back/forward cache (bfcache)

Some instabilities reappear on back/forward navigation. Making the page bfcache-compatible improves the navigation experience and can avoid reloads that reintroduce shifts.

Doc: https://web.dev/articles/optimize-cls


Real-world cases

The hero image arrives late and pushes the H1

The title jumps down when the image loads. The image has no dimensions, or the wrapper has no ratio. Put width/height on the image and a stable ratio on the wrapper.

The user aims for a link, the banner inserts itself, and they click next to it. The banner was injected above the content after the first render. Use a fixed overlay or reserve the height from the start.

Shift at the bottom of the page, often after 2-5 seconds. A third-party script injects a component into the flow. Contain the widget (fixed overlay) or reserve a zone if you display it inline.

The font changes and the text recomposes

Visible reflow on paragraphs, headings spilling onto 2 lines. Fallback and final font metrics are too different, and the font loads late. Preload the critical font, pick a close fallback, and use font-display: optional (validate UX-side).

The ad slot collapses when not filled

A big space appears then disappears, or the other way around. The slot changes height based on fill. Set a stable min-height and a fill strategy (skeleton). Avoid abrupt collapse.


Measure, track, avoid regressions

Optimizing once isn’t enough. CLS comes back fast when a marketing script changes, a new widget gets added, or a template gets “simplified”.

Prioritize the Field (Search Console / CrUX)

Use the Core Web Vitals report to spot “Poor” URL groups. Fix templates first: home, listing, article, product.

Source: https://support.google.com/webmasters/answer/9205520?hl=en

Put a safety net in pre-prod

Add a perf budget (e.g. lab CLS < 0.1 on critical pages). Run Lighthouse in CI on 3-5 representative URLs (home, listing, article, checkout). Block the merge if CLS exceeds the budget.

Instrument (advanced but worth it)

If you have significant volume, a RUM (Real User Monitoring) approach tells you which pages actually shift, on which devices, and after which events.

Google’s web-vitals library is commonly used to report CLS/LCP/INP to your analytics (GA4, Matomo, internal endpoint).

Doc: https://github.com/GoogleChrome/web-vitals


Checklist and action plan

Quick wins (under an hour)

  • Every img has width + height or a ratio reserved via aspect-ratio
  • Every iframe/embed is in a wrapper with a ratio
  • Ad slots have a min-height and don’t collapse abruptly
  • Cookie banner: fixed overlay or space reserved from the start
  • Animations: transform/opacity only

7-day plan

Day 1: list 10 “Poor” URLs in Search Console and group by template.

Day 2: Performance trace on 3 pages per template, note the 3 biggest shifts.

Day 3: fix media and embeds. Often 50% of the gain.

Day 4: fix banners and above-the-fold injections.

Day 5: fix fonts (preload + fallback + font-display).

Day 6: stabilize ads (slots, ratios, no collapse).

Day 7: set a Lighthouse budget and field monitoring.


FAQ

Does CLS really impact SEO?

Yes. CLS Google is one of the three Core Web Vitals (with LCP and INP) used as a ranking signal since 2021. On very competitive queries, two pages with equivalent content will be split by their Page Experience scores. A bad CLS SEO also plays indirectly: it hurts bounce rate and engagement, two behaviours Google observes. Source: Google Search Central - Page Experience.

What’s the acceptable threshold for CLS Google?

Less than or equal to 0.1 = good. Between 0.1 and 0.25 = needs improvement. Above 0.25 = poor (“Poor” in Search Console). It’s the 75th percentile over a rolling 28-day window that qualifies a URL.

My CLS is good in Lighthouse but bad in Search Console

Classic. Post-load shifts aren’t reproduced in lab: ads, widgets, consent, AB test. Prioritize debug on the real URL and on mobile.

Source: https://support.google.com/webmasters/answer/9205520?hl=en

Width/height or aspect-ratio: which one do I pick?

Both are complementary. width/height on img is robust and simple. aspect-ratio is ideal for wrappers, embeds and responsive slots.

Why is it worse on mobile?

More variable network, slower CPU, costlier third-party scripts, taller banners. Shifts are more visible and more frequent.

Is CLS computed over the whole lifetime of the page?

It’s grouped into session windows. A late shift can count if the window is busy.

Source: https://web.dev/blog/evolving-cls


Conclusion

Optimizing CLS is a layout discipline. Reserve space before showing (media, embeds, ads, banners, dynamic blocks) and avoid injections into the flow after initial render. Start with the field (Search Console), debug with DevTools, fix by family of causes, and set a budget to avoid regressions.

In 2025, user experience matters more than ever - see the state of SEO in 2025. And for 2026 + AI, visual stability also conditions your ability to be cited in AI Overviews.


Sources