Cumulative Layout Shift (CLS) is the Core Web Vital that measures visual stability — how much the page's content unexpectedly moves during loading. High CLS means elements jump around, text moves after you've started reading it, and buttons shift right as you're about to click them. It's one of the most frustrating user experience issues, and it directly impacts Google search rankings.
Understanding the CLS Score
CLS is a unitless score calculated from the sum of layout shift scores throughout the page's entire lifetime:
Layout Shift Score = Impact Fraction × Distance Fraction
Impact Fraction = Fraction of viewport affected by the shift
Distance Fraction = Largest distance shifted as a fraction of viewport height
CLS = Sum of all layout shift scores (excluding shifts within 500ms of user input)
CLS thresholds: | Score | Rating | Impact | |-------|--------|--------| | ≤ 0.1 | Good | Minimal visual disruption | | 0.1 – 0.25 | Needs Improvement | Noticeable shifts | | > 0.25 | Poor | Severe visual instability |
Note: CLS excludes shifts that happen within 500ms of user input (taps, clicks). A layout shift caused by user interaction (clicking a button that expands content) doesn't count against your score — only unexpected shifts count.
Common Causes of Layout Shift
1. Images Without Dimensions
The most common CLS cause. When a browser loads an image without knowing its dimensions, it allocates zero space for it initially. When the image loads, everything below it shifts down.
<!-- BAD: No dimensions specified -->
<img src="product.jpg" alt="Product">
<!-- GOOD: Explicit dimensions prevent layout shift -->
<img src="product.jpg" alt="Product" width="800" height="600">
Or use CSS aspect-ratio for responsive images:
/* Reserve space before image loads */
.product-image {
aspect-ratio: 4/3;
width: 100%;
}
2. Ads, Embeds, and Iframes Without Reserved Space
Ad containers that expand when an ad loads push content down. This is extremely common and very impactful on CLS scores.
/* Reserve space for ad slot */
.ad-container {
min-height: 250px; /* Expected ad height */
width: 100%;
}
3. Dynamically Injected Content Above Existing Content
Pop-ups, banners, cookie notices, and other dynamically injected content that appears above existing page content causes everything below it to shift.
Solutions:
- Reserve space for these elements in the initial layout
- Use transforms/overlay positioning that doesn't affect flow
- Use fixed positioning for banners that would otherwise push content
4. Web Fonts Causing FOUT/FOIT
Flash of Unstyled Text (FOUT) or Flash of Invisible Text (FOIT) occurs when web fonts load after text has been displayed in a fallback font. The font swap changes text dimensions, causing layout shifts.
/* Reduce font swap impact */
@font-face {
font-display: optional; /* Don't show font until available, no swap */
/* OR: */
font-display: swap; /* Show fallback immediately, swap when ready */
}
Better: Preload critical fonts to reduce the time until the correct font is available:
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
5. Animations That Use Non-Transform Properties
Animating top, left, margin, padding, or width/height triggers layout recalculation and can cause layout shifts.
/* BAD: Causes layout shift */
.slide-in {
animation: slide 0.3s;
}
@keyframes slide {
from { margin-top: -100px; }
to { margin-top: 0; }
}
/* GOOD: Transforms don't affect layout */
.slide-in {
animation: slide 0.3s;
}
@keyframes slide {
from { transform: translateY(-100px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
Diagnosing High CLS
Use Chrome DevTools to identify which elements are shifting:
- Open DevTools → More Tools → Rendering
- Enable "Layout Shift Regions" (shows red rectangles on shifted elements)
- Reload the page and observe which elements shift
Alternatively, use the web-vitals library with attribution data:
import { onCLS } from 'web-vitals/attribution';
onCLS(({ value, attribution }) => {
console.log('CLS score:', value);
attribution.largestShiftEntry?.sources?.forEach(source => {
console.log('Shifted element:', source.node);
console.log('Shift value:', source.currentRect, '→', source.previousRect);
});
});
Continuous CLS Monitoring
Lab Monitoring
CLS is measured in lab tools by running the page through a simulated load. AzMonitor's performance monitoring captures CLS alongside other Core Web Vitals:
performance_monitor:
url: https://yoursite.com
capture_metrics: [cls, lcp, inp, fcp, ttfb]
cls_threshold: 0.1
alert_if_above_threshold: true
frequency: daily # Or after each deployment
Important caveat: Lab CLS measurements may miss issues that only occur with real user behavior (scrolling, dynamic content based on user data). Field data from RUM is essential for complete CLS monitoring.
Field Data Monitoring
Real CLS issues often appear in specific scenarios: slow network connections where ads load late, specific user journeys that trigger dynamic content, or device types where fonts load differently.
import { onCLS } from 'web-vitals';
onCLS(({ value, id }) => {
// Track segmented by page and user context
fetch('/analytics/vitals', {
method: 'POST',
body: JSON.stringify({
metric: 'cls',
value,
id,
url: window.location.pathname,
deviceType: /Mobile|iPhone|Android/i.test(navigator.userAgent) ? 'mobile' : 'desktop',
}),
headers: { 'Content-Type': 'application/json' }
});
}, { reportAllChanges: true }); // Report each individual shift, not just final
CLS in Single Page Applications
SPAs have unique CLS challenges. Page transitions, data loading, and component mounting can all cause layout shifts:
Route transitions: When navigating to a new "page" in a SPA, new content loads asynchronously. Without skeleton screens or proper loading states, content shifts as it loads.
Data fetching: Components that fetch data and then expand to show results cause layout shift if no placeholder space is reserved.
// React example: Skeleton loading to prevent CLS
function ProductCard({ productId }) {
const { product, loading } = useProduct(productId);
if (loading) {
return (
// Fixed-height skeleton that reserves space
<div className="product-card skeleton" style={{ height: '320px' }} />
);
}
return (
<div className="product-card">
{/* product content */}
</div>
);
}
Setting CLS Targets by Page Type
Different page types have different CLS risk profiles:
| Page Type | CLS Risk | Target | |-----------|---------|--------| | Static marketing pages | Low | < 0.05 | | Blog/content pages | Medium (ads) | < 0.1 | | E-commerce product pages | High (images, reviews) | < 0.1 | | SPA dashboards | High (data loading) | < 0.1 | | News pages | Very high (ads, embeds) | < 0.1 (hard to achieve) |
Start monitoring CLS with AzMonitor alongside your other Core Web Vitals. Catch CLS regressions from deployment changes before they affect your Google search rankings.
Related: Core Web Vitals monitoring guide for complete CWV coverage.
3 monitors free forever · No credit card needed · Set up in 2 minutes
Start monitoring free →