/ Learning Hub / CLS Debugger Guide

CLS Debugger: Find Every Layout Shift Source

Cumulative Layout Shift is the most fixable Core Web Vital — once you find the culprits. The challenge is finding them: CLS happens during the entire page lifetime, not just at load, and visual debugging is slow. The CLS Debugger records every layout shift event with the element responsible, the magnitude, and the moment it happened. This guide covers the eight common CLS sources, how the debugger surfaces each, and the canonical fixes.

What is CLS?

CLS measures unexpected layout shifts during page load and interaction. Each shift is scored by impact fraction (% of viewport affected) × distance fraction (how far things moved). The cumulative score across the page session is your CLS. Google\'s benchmark: good < 0.1, needs improvement 0.1-0.25, poor > 0.25.

💡 CLS only counts "unexpected" shifts. Shifts caused by user input (clicking a "Show more" button) within 500ms are excluded. The debugger filters these correctly so you focus on involuntary shifts.

The eight common CLS sources

Images without dimensionsImage loads, layout reflows to its actual size, content below shifts down.
Late-loading web fontsFOUT (Flash of Unstyled Text) shifts everything when the web font swaps in.
Dynamic ad slotsAd iframe loads at unknown size; reserves a default, then expands.
Injected componentsCookie banner, support chat, newsletter modal push content down on appearance.
Responsive ratiosImages that change aspect ratio at breakpoints without proper containment.
Video without containerVideo player loads, player chrome adds, video starts, aspect changes.
Web componentsCustom elements that render late, often with no reserved space.
CSS animation triggersLayout-affecting animations (height, width, margin) instead of compositor-friendly transforms.

Fix 1: Always declare image dimensions

The single biggest CLS win: every <img> and <video> needs explicit width and height attributes — even when the image is responsive.

<!-- Bad: layout shifts when image loads -->
<img src="/hero.jpg" alt="Hero">

<!-- Good: browser reserves space immediately -->
<img src="/hero.jpg" alt="Hero" width="1600" height="900">

<!-- Good for responsive: aspect-ratio reserves the space -->
<img src="/hero.jpg" alt="Hero" width="1600" height="900"
     style="width: 100%; height: auto;">

The width/height attributes provide the aspect ratio. Modern browsers use this to reserve the right amount of space before the image loads, eliminating shift.

Fix 2: Font loading strategy

Web fonts cause shifts in three ways: FOUT (text renders in fallback, then swaps to web font, shifts), FOIT (text invisible until web font arrives, then appears causing shift), or proper sizing mismatch between fallback and web font.

/* Use size-adjust to match fallback font metrics */
@font-face {
  font-family: 'YourFont';
  src: url('/fonts/your-font.woff2') format('woff2');
  font-display: swap;
  size-adjust: 105%;
  ascent-override: 90%;
  descent-override: 20%;
  line-gap-override: 0%;
}

body {
  font-family: 'YourFont', system-ui, sans-serif;
}

The size-adjust, ascent-override, etc. properties tune the fallback font\'s metrics to match the web font, eliminating shift when the swap happens. Chrome\'s font fallback calculator can compute the right values.

Fix 3: Reserve space for ads

Ad slots that load dynamically need explicit dimensions reserved before the ad arrives:

<div class="ad-slot" style="min-height: 250px; width: 300px; display: flex; align-items: center; justify-content: center;">
  <!-- Ad SDK injects here -->
</div>

For ad slots that may serve multiple sizes, reserve the largest expected size — wasting some whitespace is better than shifting.

Fix 4: Inject components OUT-of-flow

Cookie banners, support chat, sticky footers — anything that appears AFTER page load — should use position: fixed or position: sticky so they don\'t push existing content:

.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
}

.support-chat {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 1000;
}

Fix 5: aspect-ratio for responsive media

Modern CSS\'s aspect-ratio property reserves space based on a ratio rather than fixed dimensions:

.responsive-video {
  aspect-ratio: 16 / 9;
  width: 100%;
}

.product-image {
  aspect-ratio: 1 / 1;
  width: 100%;
  object-fit: cover;
}

Fix 6: Reserve space for video

Video players (YouTube iframe, Vimeo, native HTML5 with chrome) need their container sized before the player loads. Use aspect-ratio or explicit dimensions on the wrapper, not just the video element.

Fix 7: Web component placeholders

Custom elements that render after page load should declare expected dimensions in CSS so the browser reserves space:

my-product-card {
  display: block;
  min-height: 300px;
  contain: layout;
}

my-product-card:not(:defined) {
  height: 300px;
  background: #f3f4f6;
}

Fix 8: Animate transform, not layout

Animations that change top, left, width, height, margin trigger layout for every frame and pollute CLS. Animate transform and opacity instead — they\'re composited and don\'t trigger layout:

/* Bad: causes layout shift */
.slide-in {
  animation: slide 0.3s ease;
}
@keyframes slide {
  from { left: -100px; }
  to { left: 0; }
}

/* Good: composited, no layout shift */
.slide-in {
  animation: slide 0.3s ease;
}
@keyframes slide {
  from { transform: translateX(-100px); }
  to { transform: translateX(0); }
}

Debugging workflow

  1. Run the CLS Debugger against the URL — get a list of every shift event
  2. For each shift, identify the element that moved and the cause
  3. Group shifts by cause (images, fonts, ads, etc.)
  4. Fix in order of largest contribution to CLS
  5. Re-test after each fix — verify CLS dropped and no new shifts introduced
  6. After all fixes, check field data 28 days later to confirm real-world improvement

Frequently Asked Questions

Why is my CLS score worse on mobile than desktop?
Mobile typically shows higher CLS because viewport-relative shift impact is larger (a 100px shift on a 375px-wide phone is bigger as a fraction than on a 1920px desktop), and mobile fonts/images often load over slower connections so the shift window is longer. Always optimise CLS against the mobile viewport — desktop usually follows.
Does using a CSS framework automatically prevent CLS?
No. Frameworks help with layout primitives but don\'t solve image dimensions, late-loading fonts, ad injection, or dynamic content. You still need to apply CLS best practices — width/height on images, font-display:swap with size-adjust, reserved ad space, fixed-position injected components. The framework just makes the CSS slightly easier to write.
Can lazy-loaded images cause CLS?
Yes if they don\'t have dimensions, even with native lazy-loading. The browser reserves the wrong space until the image loads, then reflows. Always add width/height attributes to lazy-loaded images — lazy loading saves bandwidth, dimensions prevent shift. They\'re complementary, not alternatives.
How is INP different from CLS?
INP measures interaction responsiveness — how fast does the page respond to clicks and key presses? CLS measures visual stability — how much does the layout move during the visit? They have different causes (INP = JS main-thread work; CLS = layout reflow) and different fixes (INP = code splitting and defer; CLS = dimensions and font strategy). A page can have great INP and terrible CLS or vice versa.

🔎 Debug your layout shifts

Identify every layout shift source — images, fonts, ads, components — with line-level diagnostics.

Run CLS Debugger →
Related Guides: How to Fix CLS Debugger Findings  ·  Core Web Vitals Guide  ·  Page Speed Guide  ·  Image Optimisation Guide
💬 Got a problem?