OctaneAudit
Core Web Vitals

Understanding Cumulative Layout Shift: Causes, Debugging and Fixes

CLS is the easiest Core Web Vital to fix once you know which elements move. Here is how the score is calculated, how to find the shifting nodes, and the fix for each common cause.

OCOctaneAudit Team6 min read
Core Web Vitals

Understanding Cumulative Layout Shift: Causes, Debugging and Fixes

You tap "Add to cart" and the page jumps; you have just tapped "Delete". That is a layout shift, and Cumulative Layout Shift is Google's way of measuring how often it happens to your users.

Of the three Core Web Vitals, CLS is the one with the most mechanical fixes. Almost every shift traces back to a single rule being broken: the browser did not know how much space something would take before it arrived.

How the score is calculated

A layout shift happens when a visible element changes its start position between two frames. Each shift gets a score:

layout shift score = impact fraction × distance fraction
  • Impact fraction: the share of the viewport occupied by unstable elements, before and after the shift combined.
  • Distance fraction: the largest distance any unstable element moved, divided by the viewport's largest dimension.

If a banner that covers 50% of the viewport pushes content down by 25% of the viewport height, the score is 0.5 × 0.25 = 0.125, already above the 0.1 threshold on its own.

CLS is the largest burst of shifts in a session, where a burst is a group of shifts less than 1 second apart with a maximum window of 5 seconds. This "session window" model means a page that shifts once on load and never again is scored on that one shift, while a page that shifts continuously during scroll accumulates.

Shifts that occur within 500 milliseconds of a user input (click, tap, key press) are excluded, because the user expects the layout to respond. Scroll is not an input for this purpose.

Finding the shifting elements

In Chrome DevTools

  1. Open the Performance panel, enable "Web Vitals" in the settings gear, and record a page load with mobile emulation.
  2. Look for the Layout Shifts track. Each red bar is a shift; click it to see the score and the affected nodes.
  3. Alternatively, open the Rendering drawer and enable Layout Shift Regions. Shifting areas flash blue as you load and scroll.

In the field

Use the attribution build of the web-vitals library. It reports the largestShiftTarget, a CSS selector for the element that contributed most to the worst burst.

import { onCLS } from "web-vitals/attribution"

onCLS(({ value, attribution }) => {
  console.log(value, attribution.largestShiftTarget, attribution.largestShiftTime)
})

Ship this to analytics and group by selector. The top three selectors usually account for most of your CLS.

In PageSpeed Insights

The "Avoid large layout shifts" diagnostic lists the top contributing elements for the lab run. Useful, but remember it misses shifts that only happen with real consent banners, personalization or ads.

Cause 1: Images and media without dimensions

The browser cannot reserve space for an image until it knows the aspect ratio. Without width and height, the image area is zero until the file arrives, then everything below it jumps.

<!-- Bad -->
<img src="/hero.avif" alt="...">

<!-- Good: the browser computes the aspect ratio before download -->
<img src="/hero.avif" width="1600" height="900" alt="..." style="width:100%;height:auto">

The same applies to <iframe> elements and other embeds. For responsive images with different aspect ratios per breakpoint, use CSS aspect-ratio on the container.

Framework image components (Next.js <Image>, Nuxt <NuxtImg>) enforce dimensions for this reason. If you use fill mode, the parent must have an explicit size.

Cause 2: Late-loading content inserted above existing content

Cookie banners, promotional bars, "app install" prompts and personalization blocks that are injected at the top of the page after load push everything down.

Fixes, in order of preference:

  1. Render it server-side so it is in the initial layout.
  2. Reserve the space with a fixed-height container that the content fills.
  3. Overlay instead of insert: position the banner with position: fixed so it does not affect document flow.
  4. Insert below the fold or only in response to user action.

Cause 3: Advertisements and embeds

Ad slots are the biggest CLS source on publisher sites because sizes are decided at auction time.

  • Give every slot a min-height equal to the most common creative size for that placement.
  • Do not collapse the slot when no ad fills; keep the reserved space or show house content.
  • Avoid inserting ads between paragraphs after the page has rendered.
.ad-slot-leaderboard { min-height: 90px; }
.ad-slot-rectangle { min-height: 250px; }

Cause 4: Web fonts

When a web font loads after text has rendered in a fallback font, the text reflows. If the fallback and the web font have different metrics, lines wrap differently and paragraphs change height. This is a FOUT shift.

  • Preload the primary font file: <link rel="preload" as="font" type="font/woff2" href="/fonts/inter.woff2" crossorigin>.
  • Use font-display: optional for body text if you can accept the fallback on slow connections. The browser then never swaps mid-render.
  • Or keep swap but match metrics with size-adjust, ascent-override and descent-override on a fallback @font-face so the swap does not change line heights.
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}
body { font-family: Inter, "Inter Fallback", sans-serif; }

Next.js next/font and similar tools generate these overrides automatically.

Cause 5: Animations that trigger layout

Animating top, left, width, height or margin changes layout on every frame and counts as shifts. Animate transform and opacity instead; they are composited and do not move other elements.

/* Bad */
.toast { transition: top 300ms; }
/* Good */
.toast { transition: transform 300ms; }

Cause 6: Content that resizes itself

Carousels that change height per slide, accordions that open on load, "Read more" blocks that expand automatically, and tables that reflow when data arrives. Give these a fixed height or a skeleton with the final dimensions.

A debugging checklist

  1. Record a mobile Performance trace with throttling and note every red bar in Layout Shifts.
  2. For each, identify the node and the cause from the six above.
  3. Check the page with a cookie banner showing, with ads loading, and logged in. Lab runs miss these.
  4. Fix the template. Re-record. Repeat until no shifts remain during load.
  5. Scroll the full page: lazy-loaded images without dimensions shift during scroll too.

What CLS does to conversion

In our client data, pages that moved from "Poor" to "Good" CLS saw mis-click rates on primary CTAs drop by between 30 and 60%. Users who misclick rarely retry; they leave. CLS is the Core Web Vital with the most direct line to revenue on transactional pages.

Summary

  • CLS is impact × distance, summed over the worst 5-second burst, excluding shifts right after input.
  • Find shifting nodes with the DevTools Layout Shifts track or the web-vitals attribution build.
  • Every fix is a variant of "reserve the space before the content arrives": dimensions on media, min-heights on slots, metric-matched font fallbacks, overlays instead of inserts, composited animations.
SharePostLinkedInFacebook

Get Your SEO Audit

Every issue in this article, checked on your site by an auditor and delivered as a prioritized roadmap within 48-72 hours.

See pricing

Related articles