Cumulative Layout Shift (CLS)
The metric that measures visual stability and unexpected layout shifts.
Check Your Site's Cumulative Layout Shift
Enter your website URL on the homepage to see your real CLS performance data.
What is CLS?
Cumulative Layout Shift (CLS) measures the sum of all unexpected layout shifts that occur during the entire lifespan of a page. A layout shift occurs when a visible element changes its position from one rendered frame to the next.
CLS Thresholds
≤ 0.1
Good
≤ 0.25
Needs Improvement
> 0.25
Poor
Quick Wins for CLS
- Add
widthandheightto all images - Use
aspect-ratioCSS property for responsive media - Set
min-heighton ad/embed containers - Add
font-display: optionalto web fonts - Never insert content above existing content
How CLS is Calculated
The layout shift score is calculated by multiplying two factors:
Layout Shift Score = Impact Fraction × Distance FractionImpact Fraction
The combined area of all unstable elements in the viewport, divided by the total viewport area.
Distance Fraction
The greatest distance any unstable element moved, divided by the viewport's largest dimension.
Note: Layout shifts that occur within 500ms of user input are excluded from CLS calculations, as users expect the page to respond to their actions.
Common Causes of Poor CLS
1. Images Without Dimensions
When images load without explicit width and height attributes, the browser doesn't know how much space to reserve, causing content to shift.
2. Ads, Embeds, and Iframes
Third-party content often loads asynchronously with unknown dimensions, pushing existing content around when it appears.
3. Dynamically Injected Content
Content added to the DOM above existing content (like banners, cookie notices, or notifications) shifts everything below it down.
4. Web Fonts Causing FOIT/FOUT
When custom fonts load and replace fallback fonts, differences in sizing can cause text to shift (Flash of Invisible/Unstyled Text).
5. Animations Triggering Layout
CSS animations that change properties like width,height, or toptrigger layout recalculations and shifts.
How to Improve CLS
1. Always Include Size Attributes
- Set
widthandheighton images and videos - Use CSS
aspect-ratiofor responsive sizing - Reserve space for ads with min-height containers
Example: Image with dimensions
<!-- HTML with explicit dimensions -->
<img src="hero.jpg" width="800" height="450" alt="Hero">
<!-- CSS aspect-ratio for responsive -->
<style>
.responsive-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
</style>2. Reserve Space for Dynamic Content
- Use placeholder containers with fixed dimensions
- Add skeleton screens while loading
- Position dynamic content at the bottom or in overlays
Example: Ad container with reserved space
.ad-container {
min-height: 250px; /* Reserve space for standard ad */
background: #f0f0f0;
}
.skeleton {
background: linear-gradient(
90deg,
#e0e0e0 25%,
#f0f0f0 50%,
#e0e0e0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}3. Optimize Web Font Loading
- Use
font-display: optionalorswap - Preload critical fonts
- Use fallback fonts with similar metrics
- Consider using system fonts for body text
Example: Font loading best practices
<!-- Preload critical font -->
<link rel="preload" href="/fonts/brand.woff2"
as="font" type="font/woff2" crossorigin>
<!-- CSS with fallback -->
@font-face {
font-family: 'Brand';
src: url('/fonts/brand.woff2') format('woff2');
font-display: optional; /* Prevents FOUT */
size-adjust: 100.5%; /* Match fallback size */
}4. Use Transform for Animations
- Use
transformandopacityfor animations - Avoid animating
width,height,top,left - Use CSS
will-changefor complex animations
Good vs Bad animations
/* Bad: Triggers layout */ .slide-in { animation: slideIn 0.3s; } @keyframes slideIn { from { left: -100px; } to { left: 0; } } /* Good: Uses transform (no layout) */ .slide-in { animation: slideIn 0.3s; } @keyframes slideIn { from { transform: translateX(-100px); } to { transform: translateX(0); } }
Measuring CLS
Using the web-vitals library
import { onCLS } from 'web-vitals';
onCLS((metric) => {
console.log('CLS:', metric.value);
// Log the sources of layout shift
metric.entries.forEach(entry => {
entry.sources?.forEach(source => {
console.log('Shifted element:', source.node);
});
});
// Send to analytics
analytics.track('CLS', {
value: metric.value,
rating: metric.rating,
});
});