What Is Cumulative Layout Shift and Why Does It Matter on Mobile?
Cumulative Layout Shift (CLS) is a Core Web Vital metric that measures how much visible content unexpectedly moves around on a page while it loads. On mobile devices, where screen real estate is limited and users interact with content almost immediately, even a small layout shift can cause frustration, accidental taps, and a poor user experience.
Google uses CLS as a ranking signal. A good CLS score is 0.1 or less. Anything above 0.25 is considered poor. If your mobile pages score above 0.1, you are likely losing both rankings and conversions.
In this guide, we will walk through the most common causes of CLS on mobile and give you specific, copy-paste code fixes and a testing workflow so you can measurably bring your score down.
How CLS Is Calculated
CLS is calculated by multiplying two values for every unexpected layout shift:
- Impact fraction: The percentage of the viewport affected by the shift.
- Distance fraction: The distance the element moved, as a proportion of the viewport.
The final CLS score is the sum of all individual layout shift scores that are not caused by user interaction (like a tap or click). On mobile, shifts tend to produce higher scores because the viewport is smaller, so even a 50px shift represents a much larger fraction of the screen.
The Most Common Causes of CLS on Mobile
Before jumping into fixes, here is a quick overview of the usual suspects:
| Cause | Impact Level | How Common |
|---|---|---|
| Images and videos without dimensions | High | Very common |
| Dynamically injected ads | High | Very common |
| Web font loading (FOIT/FOUT) | Medium | Common |
| Late-loading banners or notification bars | High | Common |
| Content injected by JavaScript | Medium to High | Common |
| Animations and transitions without transform | Low to Medium | Occasional |
Now let us fix each one.
Fix 1: Set Explicit Dimensions on Images and Videos
The problem
When an image or video element loads without width and height attributes, the browser cannot reserve the correct amount of space. The content below the media jumps down once the asset finishes loading.
On mobile, this is the number one cause of CLS.
The fix
Always include width and height attributes directly in your HTML. Modern browsers use these to calculate the aspect ratio before the image loads.
<img src="hero.webp" width="800" height="450" alt="Product hero image" loading="lazy">
Then, in your CSS, ensure images are responsive:
img {
max-width: 100%;
height: auto;
}
This combination tells the browser the aspect ratio (800:450) and allows it to reserve the exact space needed, even on different screen sizes.
For responsive art direction
If you use the <picture> element with different crops for mobile and desktop, set width and height on the inner <img> tag that matches the most common source:
<picture>
<source media="(max-width: 768px)" srcset="hero-mobile.webp" width="400" height="400">
<img src="hero-desktop.webp" width="800" height="450" alt="Hero">
</picture>
Using aspect-ratio as a fallback
For containers where you cannot set HTML attributes (such as CMS-generated content), use the CSS aspect-ratio property:
.image-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
overflow: hidden;
}
Fix 2: Reserve Space for Ads and Dynamically Injected Content
The problem
Ad slots, especially from third-party networks, are one of the trickiest CLS sources. The ad script loads asynchronously, and when the creative finally renders, it pushes surrounding content down or sideways.
The fix
- Set a minimum height on the ad container. Look at your most frequently served ad sizes and apply a
min-heightthat matches.
.ad-slot-mobile {
min-height: 250px; /* Common 300x250 ad unit */
width: 100%;
background-color: #f0f0f0; /* Optional placeholder color */
}
- Use CSS containment to tell the browser this container will not affect the layout of siblings:
.ad-slot-mobile {
contain: layout style paint;
min-height: 250px;
}
- Avoid inserting ads above the fold after page load. If you must place an ad near the top of the mobile page, load it as early as possible in the HTML rather than injecting it with delayed JavaScript.
- For collapsible ads (ads that sometimes do not fill), use a CSS transition instead of removing the element entirely, so the collapse does not count as a large shift.
Fix 3: Eliminate Font-Induced Layout Shifts
The problem
Custom web fonts load after the initial render. Depending on your font-display strategy, you may see:
- FOIT (Flash of Invisible Text): Text is hidden until the font loads, then appears and shifts surrounding elements.
- FOUT (Flash of Unstyled Text): A fallback font renders first, then swaps to the custom font. If the two fonts have different metrics, everything shifts.
The fix
Step A: Use font-display: optional
This tells the browser to use the custom font only if it is already cached. If not, it sticks with the fallback. Zero layout shift.
@font-face {
font-family: 'CustomSans';
src: url('/fonts/custom-sans.woff2') format('woff2');
font-display: optional;
}
This is the most aggressive CLS fix for fonts. The tradeoff is that first-time visitors may not see your custom font.
Step B: If you need font-display: swap, match fallback metrics
Use the CSS size-adjust, ascent-override, descent-override, and line-gap-override descriptors to make your fallback font occupy the same space as the custom font:
@font-face {
font-family: 'CustomSans Fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'CustomSans', 'CustomSans Fallback', sans-serif;
}
Tools like Fontaine or the Next.js font optimization module can generate these values automatically.
Step C: Preload your fonts
<link rel="preload" href="/fonts/custom-sans.woff2" as="font" type="font/woff2" crossorigin>
Preloading ensures the font file starts downloading early, reducing the window in which a swap can occur.
Fix 4: Handle Late-Loading Banners and Notification Bars
The problem
Cookie consent banners, promotional bars, and app-install prompts often inject themselves at the top or bottom of the page after JavaScript runs. If they push existing content, they cause CLS.
The fix
- Use overlay positioning. Place the banner with
position: fixedorposition: stickyso it does not affect the document flow at all.
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 1000;
}
- If the banner must push content (for legal reasons, for example), reserve space for it in the initial HTML using a placeholder div with a set height, and remove it only after the banner loads.
Fix 5: Avoid JavaScript-Driven DOM Insertions Above Visible Content
The problem
Single-page applications and dynamic content loading (infinite scroll, skeleton screens that change size, lazy-loaded components) can all cause shifts when new elements are inserted into the DOM above the user’s current scroll position.
The fix
- Insert new content below the viewport whenever possible. If a user is scrolled to a certain point, append new items below, not above.
- Use skeleton placeholders with fixed dimensions. If you show a loading skeleton, make sure it is the same height and width as the final content.
- Use the CSS
containproperty on containers that will receive dynamic content:
.dynamic-section {
contain: layout;
min-height: 300px;
}
Fix 6: Use CSS Transform for Animations
The problem
Animating properties like top, left, width, or height can trigger layout recalculations and contribute to CLS.
The fix
Replace layout-triggering properties with transform and opacity, which run on the compositor thread and do not cause layout shifts:
/* Bad */
.slide-in {
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { left: -100%; }
to { left: 0; }
}
/* Good */
.slide-in {
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
How to Test and Measure CLS on Mobile
Fixing CLS without measuring it is guesswork. Here is a reliable testing workflow.
Method 1: Chrome DevTools Performance Panel
- Open Chrome DevTools (F12 or right-click and Inspect).
- Click the three-dot menu in DevTools and select More tools > Rendering.
- Check the box labeled Layout Shift Regions. Shifting elements will flash blue as you load the page.
- To simulate mobile, click the device toggle toolbar (the phone/tablet icon) and select a mobile device preset.
- Go to the Performance tab, click Record, reload the page, and stop recording after the page is fully loaded.
- In the timeline, look for the Experience section. Each layout shift event will appear as a red or orange bar.
- Click on a shift event to see exactly which element moved and by how much.
Method 2: PageSpeed Insights
- Go to pagespeed.web.dev.
- Enter your URL and run the test.
- Look at the Core Web Vitals section. PageSpeed Insights shows both field data (real user metrics from the Chrome User Experience Report) and lab data (Lighthouse simulation).
- Focus on the field data for CLS because lab tests do not always capture ad-related or JavaScript-driven shifts that happen in real browsing.
- Scroll down to the Diagnostics section and look for the “Avoid large layout shifts” audit. It will list the exact elements causing shifts.
Method 3: Web Vitals JavaScript Library
For ongoing monitoring, add the web-vitals library to your site:
import {onCLS} from 'web-vitals';
onCLS(console.log);
This gives you real-user CLS data that you can send to your analytics platform to track improvements over time.
Method 4: Search Console Core Web Vitals Report
- Open Google Search Console for your property.
- Navigate to Core Web Vitals under the Experience section.
- Switch to the Mobile tab.
- URLs are grouped by status: Good, Needs Improvement, or Poor. Click into any group to see which pages are affected and why.
A Complete CLS Debugging Checklist for Mobile
Use this checklist after applying fixes to confirm you have covered everything:
| Item | Status |
|---|---|
| All images have width and height attributes | ☐ |
| All videos and iframes have explicit dimensions | ☐ |
| Ad containers have min-height set | ☐ |
| Fonts use font-display: optional or have metric overrides | ☐ |
| Critical fonts are preloaded | ☐ |
| Cookie/consent banners use fixed or sticky positioning | ☐ |
| No content is injected above the viewport after load | ☐ |
| Animations use transform instead of layout properties | ☐ |
| Skeleton screens match final content dimensions | ☐ |
| CLS score under 0.1 in PageSpeed Insights (mobile) | ☐ |
Real-World Example: Reducing CLS from 0.38 to 0.04
To illustrate how these fixes work together, here is a condensed case study from a content-heavy mobile site we worked on:
- Starting CLS: 0.38 (Poor)
- Biggest culprits identified via DevTools: Hero image without dimensions (0.15 shift), ad slot injected after load (0.12 shift), font swap causing text reflow (0.08 shift), cookie banner pushing header (0.03 shift).
- Fixes applied:
- Added
width="375" height="250"to hero image tag plusaspect-ratio: 3 / 2on the wrapper. - Set
min-height: 250pxandcontain: layouton the ad container. - Switched to
font-display: optionaland preloaded the primary font file. - Changed cookie banner to
position: fixed; bottom: 0;.
- Added
- Result: CLS dropped to 0.04 (Good) within one release cycle.
The entire process from diagnosis to verification took less than a day.
Frequently Asked Questions
What is a good CLS score for mobile?
Google considers a CLS score of 0.1 or lower as “Good.” Scores between 0.1 and 0.25 “Need Improvement,” and anything above 0.25 is rated “Poor.” For mobile, aim as close to zero as possible because the smaller viewport amplifies every shift.
How do I check my Cumulative Layout Shift score?
You can check CLS using PageSpeed Insights, Chrome DevTools Performance panel, Google Search Console Core Web Vitals report, or the web-vitals JavaScript library. For real-user data, the Search Console report and the web-vitals library are the most reliable.
Why is my CLS worse on mobile than on desktop?
Mobile viewports are smaller, so the same pixel-level shift represents a larger fraction of the visible screen. Additionally, mobile connections are often slower, which means images, fonts, and ads take longer to load, increasing the chance of visible shifts.
Do lazy-loaded images cause CLS?
Lazy-loaded images can cause CLS if they do not have explicit width and height attributes (or an aspect-ratio set via CSS). As long as space is reserved before the image enters the viewport, lazy loading does not cause layout shifts.
Can third-party scripts cause CLS?
Yes. Third-party ad scripts, analytics widgets, chat widgets, and social embeds are common CLS sources. The best approach is to reserve space for these elements with min-height and use contain: layout on their parent containers.
How long does it take for Google to reflect CLS improvements?
Google’s Core Web Vitals field data is based on a 28-day rolling average from real Chrome users. After deploying fixes, expect to wait roughly 28 days before the Search Console report shows the full improvement.
Wrapping Up
Fixing Cumulative Layout Shift on mobile is not about one magic CSS rule. It is about systematically identifying every element that moves unexpectedly and giving the browser enough information to reserve space before content loads. Start with images and ads (they are almost always the biggest offenders), then move to fonts and dynamic content.
Use the testing workflow outlined above to measure before and after every change. With the fixes in this guide, getting your mobile CLS below 0.1 is entirely achievable, often within a single sprint.
Need help diagnosing performance issues on your site? Get in touch with the FFT Guru team and we will run a full Core Web Vitals audit for you.

