Cumulative Layout Shift (CLS) frustrates users and hurts SEO rankings. Learn systematic approaches to prevent layout shifts and achieve a CLS score under 0.1.
Understanding CLS Calculation
CLS measures the sum of all unexpected layout shift scores. Each shift score = impact fraction × distance fraction.
- Impact fraction: How much viewport space the element occupies before and after the shift
- Distance fraction: How far the element moved
Expected vs Unexpected Shifts
User-initiated shifts (like clicking a button that expands content) don't count. Only unexpected shifts from:
- Images loading without reserved space
- Fonts swapping and causing reflow
- Dynamic content insertion
- Late-loading ads and embeds
Image Layout Shift Prevention
Always Set Dimensions
<!-- Bad: No dimensions -->
<img src="photo.jpg" alt="Photo">
<!-- Good: Explicit dimensions -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- Better: Aspect ratio with CSS -->
<img src="photo.jpg" style="aspect-ratio: 4/3; width: 100%;" alt="Photo">
Responsive Image Containers
.image-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
}
.image-container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
Font Loading CLS Prevention
Strategy 1: font-display: optional
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: optional;
}
This uses the fallback font if the custom font isn't cached. Zero CLS but users may never see the custom font on first visit.
Strategy 2: Size Adjustment
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
size-adjust: 105.2%; /* Match fallback font metrics */
ascent-override: 98%;
descent-override: 25%;
line-gap-override: 0%;
}
Strategy 3: Font Preloading
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
Dynamic Content CLS Prevention
Reserve Space for Late-Loading Content
<!-- Reserve space for dynamically loaded content -->
<div style="min-height: 300px;" id="recommendations">
<!-- Content loads here -->
<div class="placeholder-skeleton">
<!-- Skeleton loading state -->
</div>
</div>
Ad Slot Management
<!-- Reserve fixed space for ads -->
<div class="ad-container" style="min-height: 250px;">
<div id="ad-slot-1"></div>
</div>
Avoid Inserting Content Above Existing Content
// Bad: Pushes content down
container.insertBefore(newElement, container.firstChild);
// Good: Append or replace placeholder
container.appendChild(newElement);
// Or replace a placeholder
placeholder.replaceWith(newElement);
Third-Party Widget CLS Prevention
Facade Pattern
Replace heavy embeds with lightweight previews:
<!-- YouTube facade -->
<div class="video-facade" style="aspect-ratio: 16/9;">
<button class="play-button" onclick="loadVideo()">
<img src="thumbnail.jpg" width="480" height="270" alt="Video thumbnail">
<svg class="play-icon">...</svg>
</button>
</div>
Monitoring CLS in Production
import {onCLS} from 'web-vitals';
onCLS((metric) => {
// Log CLS value and shifts
console.log('CLS:', metric.value);
metric.entries.forEach((entry) => {
entry.sources.forEach((source) => {
console.log('Shift element:', source.node);
});
});
});
CLS Optimization Checklist
- Set explicit dimensions on all images and videos
- Use aspect-ratio CSS property
- Implement font-display strategy
- Reserve space for dynamic content
- Use skeleton loading states
- Preallocate ad slots
- Implement facade pattern for embeds
- Avoid content insertion above fold
- Test with Chrome DevTools Layout Shift Regions
- Monitor CLS in production with RUM