Cumulative Layout Shift measures unexpected movement of visible page content. A late image, banner, font, or embedded widget can move a button just as someone tries to use it. To fix CLS reliably, reproduce the shift on the deployed page, identify the element that changed the layout, reserve its space before it loads, and test the same path again.
This guide goes deeper than the Core Web Vitals overview for developers. It focuses on finding the source of a layout shift, applying fixes in HTML and CSS, and keeping the result stable after release.
Cumulative Layout Shift checklist
Use this short pass on the public URL you plan to ship:
- Check field CLS for the route when enough real-user data exists.
- Record a clean mobile and desktop load in the browser Performance panel.
- Inspect the largest layout-shift cluster and the elements that moved.
- Reserve space for images, video, embeds, ads, and async components.
- Test fonts, banners, validation messages, and lazy-loaded content.
- Repeat the important scroll and interaction path, not only the initial load.
- Save a bounded lab check in CI and monitor field data after deployment.
Google's CLS documentation defines a good score as 0.1 or less at the 75th percentile of page visits, split across mobile and desktop. A score above 0.1 and up to 0.25 needs improvement; above 0.25 is poor. These are page-experience targets, not proof that every visit is stable.
What Cumulative Layout Shift measures
A layout shift occurs when a visible element changes position between rendered frames. CLS does not add every shift forever. The current metric uses the largest session window: a burst of shifts with less than one second between entries and a maximum duration of five seconds. The web.dev CLS definition explains the windowing model and the unitless shift score.
Expected movement close to a discrete user input is normally excluded. For example, expanding an accordion immediately after a click is different from a promotion appearing above the article without warning. The browser exposes this distinction through LayoutShift.hadRecentInput; MDN documents that the flag covers shifts within 500 milliseconds of qualifying input.
CLS can still appear after the load event. Common examples include lazy content that arrives while a visitor scrolls, a single-page app route that renders in stages, or a message inserted after a slow request. A quick reload is therefore only the first test.
Test CLS in field and lab data
Field and lab results answer different questions:
- Field CLS describes real page visits across devices, networks, caches, content variants, and longer sessions.
- Lab CLS reproduces a chosen load or flow under controlled conditions so a developer can debug it.
Start with field data when it is available, then use a lab trace to find the cause. A new page may not have enough field traffic, and a short lab run can miss shifts that happen after scrolling or interaction. The web.dev CLS optimization guide explains why Chrome UX Report data and a load-only lab result can differ.
For a local investigation in Chrome:
- Open DevTools on the deployed URL and select Performance.
- Use a clean profile or disable the cache, then record a reload.
- Open the Layout shifts track and select the largest cluster.
- Inspect the highlighted elements, score, timing, and suggested culprits.
- Record again while scrolling and completing the page's main task.
Chrome's Performance panel reference shows how shift clusters appear as purple markers and how to inspect the affected elements. Test a mobile width as well as desktop because responsive media, navigation, and ads can create different layouts.
Find the element that caused the layout shift
The element that moved is not always the element that caused the movement. A paragraph may shift because an image above it had no dimensions. Use the trace timing to inspect what entered or resized just before the affected element moved.
For a focused Chromium debugging pass, a PerformanceObserver can log unexpected shift entries and their sources:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue;
console.table({
value: entry.value,
time: Math.round(entry.startTime),
sources: entry.sources?.map((source) => source.node),
});
}
});
observer.observe({ type: "layout-shift", buffered: true });
This snippet lists individual unexpected shifts; it does not calculate the full session-window metric. Browser support for the Layout Instability API is not universal, so use it as debugging evidence rather than the only release gate. The MDN `hadRecentInput` reference includes the same exclusion pattern and current compatibility notes.
How to fix Cumulative Layout Shift from images and embeds
Give replaced elements a stable box before their files arrive. For images, use truthful width and height attributes so the browser can derive an aspect ratio, then let CSS resize the box responsively:
<img
src="/images/audit-report-1200.webp"
width="1200"
height="750"
alt="Audit report with performance and accessibility findings"
>
img {
max-width: 100%;
height: auto;
}
Do not copy arbitrary dimensions merely to satisfy a check. The attributes must match the source ratio, or the browser may reserve the wrong shape and shift again when CSS or the image corrects it.
Apply the same principle to video, maps, ad slots, and iframes. If an embed's exact height varies, reserve a realistic minimum for each breakpoint:
.video-frame {
aspect-ratio: 16 / 9;
width: 100%;
}
.recommendation-slot {
min-height: 18rem;
}
Keep the placeholder and final component in the same container. Removing a reserved slot when no ad or recommendation arrives can cause as much movement as inserting one. The website image optimization checklist covers responsive sources, intrinsic dimensions, transfer size, and alternative text in more detail.
Reserve space for dynamic content
Async components should claim their layout space in the first render. Match a skeleton to the final component's approximate dimensions instead of drawing a generic one-line placeholder for a large card:
.account-summary {
min-block-size: 12rem;
}
.account-summary[aria-busy="true"] {
background: linear-gradient(100deg, #eef1f4 20%, #f8f9fa 50%, #eef1f4 80%);
}
For status messages and form errors, reserve a small message region or place the message beside the field it describes. Avoid inserting a site-wide announcement above the header after the page becomes usable. If a notice must overlay content, confirm that it remains dismissible and does not block the main task; the cookie banner accessibility checklist covers that flow.
When a user action triggers a slow request, update the layout immediately with a stable loading state. Movement that begins long after the interaction can feel unrelated and may count toward CLS.
Reduce layout shifts from web fonts and animation
Use the font-display implementation and testing guide to choose between swap, fallback, and optional, then test slow and failed font loads separately.
A downloaded font can change line breaks, element heights, and everything below a text block. Load critical fonts early, choose a fallback with similar metrics, and only use font-display: optional when keeping the fallback is acceptable for the design. CSS font metric overrides such as size-adjust, ascent-override, descent-override, and line-gap-override can make a fallback occupy space closer to the final font.
Test headings, buttons, prices, and navigation labels at the breakpoints where wrapping changes. A small glyph-width difference can turn a one-line control into two lines and shift an entire toolbar.
For motion, prefer transform and opacity when they produce the intended result. Animating top, left, width, or height can change layout. Do not hide an unstable insertion behind an animation; reserve the final space first, then animate within it.
Add a CLS regression check
A CI check should reproduce one defined path and use a threshold your team owns. Keep it separate from field monitoring because a headless run cannot represent every user session.
One reliable application-level approach is to load Google's web-vitals package in the page and report onCLS values to your test or telemetry endpoint. The official `web-vitals` documentation implements the current session-window calculation and can report the entries that contributed to the metric.
Whichever runner you choose, record these test conditions with the result:
- final URL and build revision;
- viewport and device profile;
- cache, network, and CPU settings;
- interactions and scroll depth included;
- measured CLS and the largest contributing cluster.
Run the check against a preview or production-like public URL after the assets and API routes are deployed. The website regression testing checklist explains how to keep a reviewable baseline, while the website audit with GitHub Actions guide shows how to attach bounded website evidence to a pull request.
Check visual stability with Slop Audit
Run a Slop Audit on the public preview or production URL after deployment. When Browser Run is available, Slop Audit records a bounded mobile-first lab CLS signal from layout-shift entries without recent input. It reports the measured value as evidence and classifies 0.1 or less as a pass, above 0.1 through 0.25 as a warning, and above 0.25 as a failure.
That result describes the audit's browser window, not the route's field CLS or every later interaction. It may miss shifts that happen outside the bounded run, and its evidence does not replace the current session-window calculation, Chrome UX Report data, or a full performance trace. Use the report to catch an obvious launch regression, then reproduce the shift in DevTools, fix the owned component, and rerun the same URL.
CLS release checklist
- Confirm field and lab results are labeled separately.
- Record the deployed route at mobile and desktop widths with a cold cache.
- Inspect the largest shift cluster and the element that changed the layout above it.
- Add accurate dimensions to images, video, and other fixed-ratio media.
- Reserve stable space for embeds, ads, skeletons, notices, and async components.
- Compare fallback and final font metrics at wrapping breakpoints.
- Repeat the page's scroll and interaction path after the load event.
- Save the test conditions and compare them with the previous release.
- Monitor field data after deployment for shifts a bounded test cannot reproduce.
Once the page is stable, use the mobile website testing guide to check horizontal overflow, touch targets, navigation, and the complete narrow-screen task around the same components.