Cumulative Layout Shift (CLS)
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); } }