Cumulative Layout Shift (CLS)
debt(d3/e3/b5/t5)
Closest to 'default linter catches the common case' (d3). Lighthouse, PageSpeed Insights, Chrome DevTools Performance panel, and CrUX all surface CLS issues readily — these are standard, widely-used tools that any frontend developer would encounter during normal performance auditing. Detection is straightforward and automated.
Closest to 'simple parameterised fix' (e3). The quick_fix indicates adding width/height to images, reserving space for dynamic content, and swapping CSS properties — these are pattern-based fixes that touch multiple elements but follow a clear template. Not a single-line fix (e1) because it typically requires changes across multiple img/iframe tags and CSS rules, but still within one component or concern area.
Closest to 'persistent productivity tax' (b5). CLS awareness must be maintained across every new feature that adds images, ads, dynamic content, or animations. It affects the entire frontend development workflow — every new image needs dimensions, every injected element needs reserved space, every animation needs the right CSS properties. It's not architectural (b7-9) but it is a persistent consideration that shapes how content is added to pages.
Closest to 'notable trap' (t5). The misconception field explicitly states developers believe CLS only matters for slow connections, when in fact layout shifts happen regardless of connection speed. Additionally, common mistakes like using CSS top/left/margin instead of transform, or font-display causing reflow, are documented gotchas that most developers eventually learn but aren't immediately obvious.
Also Known As
TL;DR
Explanation
CLS quantifies how much page content unexpectedly moves during the loading phase, frustrating users who click on the wrong element as the page shifts. The score is calculated as the sum of impact fraction × distance fraction for each unexpected layout shift. Target: under 0.1 (Good), 0.1-0.25 (Needs Improvement), over 0.25 (Poor). Common causes: images and iframes without explicit width and height attributes (browser can't reserve space); ads, cookie banners, or embeds injected above existing content after load; web fonts that swap and cause text reflow; animations that use top/left instead of transform (which is GPU-composited and doesn't cause layout recalculation). CLS only counts unexpected shifts — layout shifts caused by user interaction (clicking a button that expands content) within 500ms of the interaction are excluded from the score.
Common Misconception
Why It Matters
Common Mistakes
- Images without explicit width and height attributes — browser cannot reserve space, images shift content when loaded.
- Ads, analytics, or cookie consent banners injected above existing content after load — always inject into reserved space.
- Web font swap causing text reflow — use font-display: optional to prevent FOUT that shifts layout.
- CSS animations using top/left/margin instead of transform — property changes that trigger layout recalculation cause CLS; transform does not.
- Dynamic content insertion above the fold without reserved height — reserve minimum height with CSS before content loads.
Avoid When
- Do not animate layout properties (margin, padding, top, left, width, height) — use transform and opacity instead, which are GPU-composited and do not trigger CLS.
When To Use
- Measure CLS in field data (CrUX) not just Lighthouse — real user device and connection conditions reveal shifts that lab tests miss.
- Use Chrome DevTools Performance panel with Layout Shift Regions overlay to visually identify what is shifting.
Code Examples
/* CLS from images without dimensions */
<img src="banner.jpg" alt="Banner"><!-- no width/height -->
/* CLS from late-injected banner */
<script>
// Runs after page load — pushes content down
document.body.prepend(cookieBanner);
</script>
/* CLS from CSS animation */
.slide-in { animation: slide 0.3s; }
@keyframes slide { from { margin-top: -100px; } to { margin-top: 0; } }
<!-- Images with explicit dimensions — browser reserves space -->
<img src="banner.jpg" alt="Banner" width="1200" height="400">
<!-- Reserved space for cookie banner -->
<div style="min-height: 60px" id="cookie-banner-slot">
<!-- Banner injects here without shifting content -->
</div>
/* Use transform instead of layout properties for animation */
.slide-in { animation: slide 0.3s; }
@keyframes slide { from { transform: translateY(-100px); } to { transform: translateY(0); } }