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.
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
- Open the Performance panel, enable "Web Vitals" in the settings gear, and record a page load with mobile emulation.
- Look for the Layout Shifts track. Each red bar is a shift; click it to see the score and the affected nodes.
- 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:
- Render it server-side so it is in the initial layout.
- Reserve the space with a fixed-height container that the content fills.
- Overlay instead of insert: position the banner with
position: fixedso it does not affect document flow. - 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-heightequal 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: optionalfor body text if you can accept the fallback on slow connections. The browser then never swaps mid-render. - Or keep
swapbut match metrics withsize-adjust,ascent-overrideanddescent-overrideon a fallback@font-faceso 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
- Record a mobile Performance trace with throttling and note every red bar in Layout Shifts.
- For each, identify the node and the cause from the six above.
- Check the page with a cookie banner showing, with ads loading, and logged in. Lab runs miss these.
- Fix the template. Re-record. Repeat until no shifts remain during load.
- 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-vitalsattribution 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.
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