What Is Cumulative Layout Shift and Why Does It Matter?
You have probably experienced it yourself. You are reading an article, about to tap a link, and suddenly the entire page jumps. A banner loads late, an image pops in without warning, or a font swap reshuffles every line of text. That frustrating experience is exactly what Cumulative Layout Shift (CLS) measures.
CLS is one of Google’s Core Web Vitals, and it directly impacts your search rankings, user experience, and conversion rates. A poor CLS score means visitors are more likely to misclick, feel disoriented, and leave your site.
In this guide, we will walk you through how to fix Cumulative Layout Shift using practical debugging steps, Chrome DevTools, and real code examples you can apply today.
What Is a Good CLS Score?
| CLS Score Range | Rating | What It Means |
|---|---|---|
| 0 – 0.1 | Good | Minimal or no unexpected layout shifts |
| 0.1 – 0.25 | Needs Improvement | Noticeable shifts that may frustrate users |
| Above 0.25 | Poor | Significant layout instability, hurts SEO and UX |
Your goal should be to keep CLS under 0.1 for at least 75% of page loads.
Step 1: Identify Your CLS Problems with Chrome DevTools
Before you fix anything, you need to know exactly what is shifting and why. Chrome DevTools gives you everything you need.
Method A: Performance Panel Recording
- Open Chrome DevTools (F12 or right-click > Inspect).
- Go to the Performance tab.
- Click the record button, then reload the page.
- Stop the recording after the page fully loads.
- Look for the “Layout Shift” entries in the Experience row. Each red or orange block represents a layout shift event.
- Click on a Layout Shift entry to see which DOM elements moved, their previous position, and their new position.
Method B: The Layout Shift Regions Flag
- In DevTools, press Ctrl+Shift+P (Cmd+Shift+P on Mac) to open the Command Menu.
- Type “Show Layout Shift Regions” and enable it.
- Reload your page. Blue rectangles will flash on screen wherever layout shifts occur.
This visual overlay makes it immediately obvious which elements are causing trouble.
Method C: Lighthouse and PageSpeed Insights
Run a Lighthouse audit directly in DevTools (Lighthouse tab) or use Google PageSpeed Insights. Both tools will flag CLS issues and point you to the specific elements responsible.
Step 2: Reserve Space for Images and Media
Images and videos loading without predefined dimensions are one of the most common causes of CLS. When the browser does not know the size of an image before it loads, it allocates zero space. Once the image downloads, everything below it gets pushed down.
The Fix: Always Set Width and Height
Add explicit width and height attributes to every <img> and <video> tag:
<img
src="/images/hero-banner.webp"
alt="Product showcase"
width="1200"
height="630"
loading="lazy"
/>
Modern browsers use these attributes to calculate the aspect ratio before the image loads, reserving the correct amount of space automatically.
Use CSS aspect-ratio for Responsive Layouts
If you use responsive images that scale to their container, combine the HTML attributes with CSS:
img {
max-width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}
Or set the aspect ratio directly:
.hero-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
This ensures images never cause layout shifts, regardless of screen size.
Handle Iframes and Embeds
Third-party embeds like YouTube videos, maps, or social media widgets are notorious for CLS. Wrap them in a container with a fixed aspect ratio:
.video-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
}
.video-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
Step 3: Stabilize Web Font Loading
When web fonts load late, the browser swaps from a fallback font to the custom font. This can change letter spacing, line height, and word wrapping, causing significant layout shifts across your entire page.
Fix 1: Use font-display: optional or swap
@font-face {
font-family: 'CustomSans';
src: url('/fonts/custom-sans.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: optional;
}
- font-display: optional prevents layout shifts entirely. If the font is not available immediately, the fallback is used for the entire page lifecycle. Best for CLS.
- font-display: swap shows the fallback first and then swaps. This can still cause shifts but ensures your custom font always appears eventually.
Fix 2: Preload Critical Fonts
Tell the browser to start downloading your fonts as early as possible:
<link
rel="preload"
href="/fonts/custom-sans.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Fix 3: Match Fallback Font Metrics
Use the CSS size-adjust, ascent-override, descent-override, and line-gap-override properties to make your fallback font match your custom font as closely as possible:
@font-face {
font-family: 'CustomSans-Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 92%;
descent-override: 22%;
line-gap-override: 0%;
}
Then reference both fonts in your stack:
body {
font-family: 'CustomSans', 'CustomSans-Fallback', Arial, sans-serif;
}
This technique dramatically reduces the visible shift when the custom font finally loads.
Step 4: Handle Dynamic Content Injection
Content that gets injected after the initial render (ads, cookie banners, notification bars, lazy-loaded components) is a major CLS offender. Here is how to fix each scenario.
Ads and Banners
- Reserve a fixed-size container for every ad slot before the ad loads.
- Use a
min-heighton the ad container that matches the expected ad dimensions. - If the ad does not fill, display a subtle placeholder or collapse the space gracefully after you know the ad will not load.
.ad-slot-leaderboard {
min-height: 250px;
width: 100%;
background-color: #f5f5f5;
}
Cookie Consent Banners
Cookie banners that push content down are a common CLS problem. The best approach is to overlay them instead of inserting them into the document flow:
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
}
Using position: fixed means the banner floats above the content and never causes a layout shift.
Lazy-Loaded Components and Infinite Scroll
- Always set a
min-heightor use a skeleton placeholder for content that loads below the fold. - Avoid inserting content above or in the middle of what the user is currently viewing.
- For infinite scroll, append new items at the bottom only.
Notification Bars and Top Banners
If you must show a promotional or notification bar at the top of the page:
- Include it in the initial HTML rather than injecting it with JavaScript.
- If it must be dynamic, use
transformanimations instead of changingheightormargin, because transforms do not trigger layout recalculations.
.promo-bar {
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.promo-bar.visible {
transform: translateY(0);
}
Step 5: Optimize CSS to Prevent Shifts
Poorly structured CSS can cause layout instability in subtle ways.
Common CSS Fixes
| Problem | Fix |
|---|---|
| Content reflows after stylesheet loads | Inline critical CSS in the <head> |
| Elements resize on viewport change | Use relative units with min-height constraints |
| Late-loading CSS overrides cause shifts | Preload critical stylesheets with <link rel="preload"> |
| Animations change layout properties | Use transform and opacity instead of width, height, top, or margin |
Inline Critical CSS Example
Extract the CSS needed for above-the-fold content and place it directly in your HTML:
<head>
<style>
.header { height: 64px; }
.hero { aspect-ratio: 16/9; width: 100%; }
.nav { min-height: 48px; }
</style>
<link rel="preload" href="/css/main.css" as="style" />
<link rel="stylesheet" href="/css/main.css" />
</head>
Step 6: Fix CLS for Server-Side Rendered and Express.js Applications
If you are building applications with Express.js, you have a unique advantage: you can control exactly what HTML is sent to the browser on the first response, eliminating many sources of CLS before the page even reaches the client.
Best Practices for Express.js Apps
- Include all critical UI elements in the initial server response. Render headers, navigation bars, and hero sections server-side instead of relying on client-side JavaScript to inject them.
- Inline critical CSS in your template. Use your Express template engine (EJS, Pug, Handlebars) to include above-the-fold styles directly in the
<head>. - Set image dimensions in your templates. Store image width and height metadata alongside your content data and output them in every
<img>tag. - Serve font files from your own domain with proper cache headers to ensure fast delivery and reduce font-swap shifts.
// Example Express.js route that includes image dimensions
app.get('/blog/:slug', async (req, res) => {
const post = await getPost(req.params.slug);
// post.heroWidth and post.heroHeight stored in database
res.render('blog-post', {
title: post.title,
heroImage: post.heroImage,
heroWidth: post.heroWidth,
heroHeight: post.heroHeight,
content: post.content
});
});
<!-- In your EJS template -->
<img
src="<%= heroImage %>"
width="<%= heroWidth %>"
height="<%= heroHeight %>"
alt="<%= title %>"
loading="eager"
/>
Step 7: Verify Your Fixes
After applying changes, you need to verify that your CLS score has actually improved. Use these tools:
- Google PageSpeed Insights for both lab data and field data (from real users via the Chrome User Experience Report).
- Chrome DevTools Lighthouse for quick local testing during development.
- Google Search Console (Core Web Vitals report) for tracking improvements across all your pages over time.
- Web Vitals JavaScript library for monitoring CLS in production with real user metrics.
Add Real User Monitoring
Install the web-vitals library and log CLS data from actual visitors:
import { onCLS } from 'web-vitals';
onCLS((metric) => {
console.log('CLS:', metric.value, metric.entries);
// Send to your analytics endpoint
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({
name: 'CLS',
value: metric.value,
page: window.location.pathname
}),
headers: { 'Content-Type': 'application/json' }
});
});
This gives you real-world data, which is ultimately what Google uses for ranking.
Quick CLS Fix Checklist
Use this checklist as a reference whenever you audit a page:
- ☑ All
<img>and<video>elements havewidthandheightattributes - ☑ Responsive images use
aspect-ratioin CSS - ☑ Web fonts use
font-display: optionalor are preloaded - ☑ Fallback font metrics are adjusted to match custom fonts
- ☑ Ad slots have reserved space with
min-height - ☑ Cookie banners use
position: fixedorposition: sticky - ☑ No content is injected above the current viewport
- ☑ Critical CSS is inlined in the
<head> - ☑ Animations use
transforminstead of layout-triggering properties - ☑ Third-party embeds are wrapped in aspect-ratio containers
Frequently Asked Questions
How do I solve cumulative layout shift?
Start by identifying which elements are shifting using Chrome DevTools Performance panel or the Layout Shift Regions overlay. Then apply the appropriate fix: set explicit dimensions for images and embeds, stabilize font loading with font-display and preloading, reserve space for ads and dynamic content, and inline critical CSS. Test your changes with Lighthouse or PageSpeed Insights to confirm improvement.
What is a good cumulative layout shift score?
Google considers a CLS score of 0.1 or less as good. Scores between 0.1 and 0.25 need improvement, and anything above 0.25 is rated poor. Aim for 0.1 or lower across at least 75% of your page loads.
Why is CLS bad for my website?
CLS creates a poor user experience because unexpected page movement causes users to click the wrong buttons, lose their reading position, and feel frustrated. Beyond UX, CLS is a Google ranking factor as part of Core Web Vitals. A poor CLS score can directly hurt your organic search visibility.
How do I fix cumulative layout shift caused by images?
Always include width and height attributes on your <img> tags. For responsive designs, combine these attributes with max-width: 100% and height: auto in your CSS. You can also use the CSS aspect-ratio property to reserve space before the image loads.
How do I fix CLS caused by web fonts?
Use font-display: optional in your @font-face declarations to prevent layout shifts entirely. Preload your font files with <link rel="preload"> so they arrive faster. Additionally, adjust your fallback font metrics using size-adjust and ascent-override to minimize the visual difference during font swap.
Can dynamic content like ads cause CLS?
Yes, ads and other dynamically injected content are among the most common causes of CLS. The fix is to always reserve space for ad slots by setting a min-height on the container element before the ad script runs. For overlays like cookie banners, use position: fixed so they do not push page content around.
How long does it take for Google to recognize CLS improvements?
Google’s Core Web Vitals data in Search Console is based on a 28-day rolling average from real users in the Chrome User Experience Report. After deploying your fixes, expect it to take approximately 28 days for the improvements to fully reflect in your CWV scores and potentially influence rankings.
