Evaluat is in private access. Demos open through July. Book a slot

Blog Web Vitals & Metrics

How to improve CLS: a step-by-step optimization guide

Almost every layout shift is the same bug: an element that loads without a reserved box, so everything below it jumps. Cumulative Layout Shift is the easiest Core Web Vital to fix, because the cure is nearly always to reserve the space in advance. This guide is the practical playbook: the fixes in priority order, the code for each, the font trick most teams skip, and how to confirm the number held for real users.

Written by: Ahmad Farzan ·

Two page layouts compared. Before, an unsized image loads and pushes the text below it down, scoring a Cumulative Layout Shift of 0.20, rated poor. After, a reserved placeholder box holds the image's space so the text stays put and CLS is 0, under the 0.1 good threshold.

To improve Cumulative Layout Shift, find what moves on the page and reserve its space before it loads. Almost every layout shift is the same bug: an element arrives without a box held for it, so everything below it jumps. Reserve that box, and the shift disappears. As of the 2025 Web Almanac, 81% of mobile pages already have a good CLS, the best pass rate of the three Core Web Vitals, yet roughly one in five still fail it, and 62% of mobile pages still ship at least one image without dimensions (2025 Web Almanac).

That last number is the whole story. CLS is the easiest Vital to fix, because the cure is almost always the same move applied in a few places. This guide is the practical playbook: the fixes in priority order, the code for each, the font trick most teams skip, and how to confirm the number held for real users. There is no separate explainer to send you to for the concept, so the first section covers what CLS is and how it is scored before the fixes begin.

First, find what shifts, and how CLS is scored

CLS measures how much visible content moves unexpectedly while a page loads. It is unitless: each shift is scored as the impact fraction times the distance fraction, the share of the viewport that moved multiplied by how far it travelled. Your CLS is not the sum of every shift but the largest burst of shifts in a session window, a group of shifts less than a second apart, capped at five seconds (web.dev). Google rates 0.1 or less as good and above 0.25 as poor, at the 75th percentile of real loads.

Before you fix anything, see what is moving. The Chrome DevTools Performance panel records each layout shift and names the element that jumped; a free page speed test with Evaluat Pulse, or a Lighthouse audit, lists the largest shifts under “Avoid large layout shifts” with the culprit nodes. Fix what the tool points at, not what you assume.

Nearly all of it traces to five causes, and they set the order of the fixes below: images without dimensions, ads and embeds without reserved space, dynamically injected content, web fonts that reflow on swap, and animations that move layout instead of pixels.

Fix, in orderThe cause it removesThe changeEffort vs payoff
Size every image and videoUnsized mediawidth/height or aspect-ratioLow effort, usually the biggest win
Reserve space for ads, embeds, iframesLate third-party contentmin-height or aspect-ratio containerLow-medium, large on ad-heavy pages
Reserve or overlay injected contentBanners and notices pushing content downPlaceholder space, or overlay instead of insertMedium
Match the fallback to the web fontFont-swap reflowfont-display plus metric overridesMedium, subtle but real
Animate with transformLayout-triggering animationtransform instead of top/heightLow

Step 1: give every image and video explicit dimensions

This is the highest-leverage fix, and the one most sites still miss. When an image has no declared size, the browser does not know how tall it will be, so it lays out the page as if the image were zero height and then shoves everything down when the file arrives. Declaring the size lets the browser reserve the exact box up front.

Set width and height attributes on every <img> and <video>. Modern browsers turn those two numbers into an aspect ratio and reserve the right space even when CSS scales the image to a different width:

<img src="/photo.avif" width="1280" height="720" alt="A clear description" />

For responsive images sized in CSS, keep the attributes and let height: auto scale them, or reserve the box directly with the aspect-ratio property:

.hero img {
  width: 100%;
  height: auto;     /* preserves the reserved aspect ratio from width/height */
  aspect-ratio: 16 / 9;
}

The room to improve here is large: 62% of mobile pages fail to set dimensions on at least one image, down from 66% in 2024 (2025 Web Almanac). On a framework you mostly get this for free, since next/image and Astro’s <Image> require dimensions and emit them for you. Verify it worked by reloading with the DevTools Performance panel recording: the image should appear with no shift logged against it.

Step 2: reserve space for ads, embeds, and iframes

Third-party content is the next cause, and it is worse than images because it loads later and you do not control its timing. An ad slot, a YouTube embed, or a social card that fills in after the text has painted will push everything below it down unless you have reserved the space.

Reserve the slot’s known dimensions before the third party arrives. For an ad unit with a fixed size, set a min-height on its container; for an embed with a known shape, use aspect-ratio:

.ad-slot      { min-height: 280px; }   /* hold the slot's height up front */
.video-embed  { aspect-ratio: 16 / 9; } /* hold an iframe's shape */

When the size is genuinely unknown, two tactics help. Place the late-loading content below the fold where a shift is less likely to land inside a session window, and never reserve too little, because a slot that grows past its min-height still shifts. If an ad can return one of several heights, reserve the most common one and accept the occasional small move rather than reserving nothing.

Step 3: stop injected content from pushing the page

Content your own code adds after load is the third cause: cookie banners, consent bars, “subscribe” prompts, promotional ribbons, and “you may also like” blocks. Each one inserted above existing content pushes that content down and scores a shift.

There are two clean fixes. Reserve the space in advance with a placeholder or skeleton the same size as the incoming content, so the real thing drops into a held box. Or overlay the element instead of inserting it, positioning a banner over the page with position: fixed or absolute so it covers content rather than displacing it. One important exception built into the metric: a shift within 500 milliseconds of a user interaction is excluded from CLS, so content that appears because someone tapped a button (an expanding menu, a “load more” click) does not count. Use that. If a banner must push the layout, trigger it from an interaction, not automatically on load.

Step 4: eliminate font-swap shifts

Web fonts cause a subtler shift. The browser paints text in a fallback font first, then swaps to your web font when it loads, and if the two fonts have different metrics the text reflows and moves the content around it. This is the cause teams notice last, because each shift is small, but on a text-heavy page they add up.

The fix is to make the fallback take the same space as the web font. Set font-display: optional so the browser does not swap mid-view, preload the critical font so it is more likely to arrive before first paint, and use the @font-face metric override descriptors to size a fallback to match (web.dev, 2025):

@font-face {
  font-family: 'Brand';
  src: url('/brand.woff2') format('woff2');
  font-display: optional;
}
@font-face {
  font-family: 'Brand-fallback';
  src: local('Arial');
  size-adjust: 107%;       /* scale the fallback to the web font's width */
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

With the fallback tuned to the same line height and width, the swap becomes invisible and the reflow disappears.

Step 5: animate with transform, not layout

The last cause is animation. Transitioning a property that affects layout, such as top, left, height, width, or margin, moves every neighbouring element on each frame and can register as shift. Animating transform (translate, scale, rotate) does not, because the browser composites the change without re-running layout:

.panel { transition: transform 0.2s ease; }  /* good: no layout, no shift */
/* avoid: .panel { transition: top 0.2s ease; }  moves neighbours each frame */

One free win sits alongside this: the back/forward cache. When a visitor navigates back, a bfcache-eligible page is restored exactly as they left it, fully laid out, so there is no reload and no fresh shift (web.dev, 2025). Keeping pages bfcache-eligible (avoid unload listeners) protects CLS on every back navigation for free.

Verify the fix held: real users, at the 75th percentile

A layout shift you fixed in the lab is not a layout shift fixed in the field. Confirm the change with real-user data, at the 75th percentile, because a single fast load on a fast machine hides the shifts that slow networks and late third parties produce.

Read field CLS with Google’s web-vitals library, which reports the value the browser actually recorded:

import { onCLS } from 'web-vitals';

onCLS(({ value, rating }) => {
  console.log(value, rating); // e.g. 0.04, "good"
});

The attribution build also names the largest shifting element, so a regression points straight at the component that caused it. CLS is the least load-sensitive Vital, since shifts are structural rather than server-bound, so it usually holds steady under concurrency while Largest Contentful Paint and Interaction to Next Paint degrade. But still measure it on real devices and networks, because a font, image, or ad that arrives late on a slow connection shifts more than it does on your desk. Core Web Vitals: lab vs field covers why the two numbers differ, and performance regression testing shows how to gate CLS in CI so a fixed shift does not quietly return.

The payoff is well documented. Yahoo! JAPAN News cut CLS from roughly 0.2 to 0 with aspect-ratio boxes and saw page views per session rise 15%, sessions run 13% longer, and bounce rate fall 1.72 points (web.dev, 2021). Smaller gains pay too: iCook improved CLS by 15% and earned 10% more ad revenue (web.dev, 2021).

Common mistakes when improving CLS

The same errors leave CLS stuck after a first pass. Each has a direct fix.

  • Sizing some images but not all. One unsized hero or thumbnail is enough to fail. Audit every <img>, not just the obvious ones.
  • Forgetting ads and iframes. Images get sized while the ad slot that loads two seconds later is ignored. Reserve every third-party box.
  • Injecting a cookie or consent banner above content. The most common avoidable shift on the web. Reserve its space or overlay it; never push the page on load.
  • Lazy-loading without reserving space. Lazy loading is good for performance and bad for CLS if the box is not held. Keep the dimensions on lazy images.
  • Setting aspect-ratio then overriding it. A later height rule can cancel the reserved box. Confirm the computed size in DevTools, do not assume the CSS won.
  • Judging CLS on desktop, or on one fast load. Mobile shifts more, because fonts, images, and ads arrive later on slower networks. Test mobile, at the 75th percentile.

Where Evaluat fits

Be honest about the tool you need here. CLS is the one Core Web Vital you can usually fix without a load test, because shifts are structural. A single real-browser audit catches most of them: Evaluat Pulse loads your page in real Chrome, reports CLS with the rest of the Vitals, and records a video so you can watch the moment something jumps, and Lighthouse or DevTools will name the shifting nodes. For debugging CLS on one page, that is enough.

Evaluat earns its place on the other two Vitals and the cases where CLS does depend on load. It runs each virtual user in an isolated real browser and captures CLS alongside Largest Contentful Paint and Interaction to Next Paint, per session, under concurrency, so you see CLS in the same run as the metrics that genuinely degrade under traffic, and you catch the layout shift that only appears when a third-party tag or ad fills in differently at peak. How it works covers the architecture, and how to improve LCP is the companion playbook for the Vital that moves most under load.

Improving CLS comes down to one habit: reserve the space before the content arrives. Size your media, hold your ad and embed slots, keep injected content from pushing the page, tune your fonts, and animate with transform. Then confirm it in the field, at the 75th percentile, where your users actually load the page.

Test in real browsers. Debug in real sessions. Book a demo.

Ahmad Farzan, Founder at Evaluat

About the author

Ahmad Farzan · Founder at Evaluat

Founder of Evaluat. Has spent years building and load-testing Adobe Commerce and Magento storefronts, and built Evaluat to test sites the way real browsers actually hit them.

More from Ahmad →

Common questions

FAQ

What is the fastest way to improve CLS?

Give every image and video an explicit width and height, or reserve their space with the CSS aspect-ratio property. Unsized media is the single most common cause of layout shift, and 62% of mobile pages still fail to size at least one image (2025 Web Almanac). The browser can then reserve the right box before the file loads, so nothing below it jumps.

What is a good CLS score?

Good is 0.1 or less; poor is above 0.25; the middle needs improvement. CLS is measured at the 75th percentile of real page loads, so a score counts as good only when at least three of every four visits stay under 0.1. CLS is unitless, a fraction of the viewport that moved, not a time.

Why is my CLS still bad after I sized the images?

Because images are only one of five causes. The usual remaining culprits are ad slots and iframes with no reserved height, content injected above the fold (cookie banners, notifications), and web fonts that reflow text when they swap in. Open the DevTools Performance panel, which flags each layout shift and names the element that moved.

Does the CSS aspect-ratio property fix CLS?

Yes, for media whose proportions you know. Setting aspect-ratio (or width and height attributes, which the browser turns into an aspect ratio) lets the browser reserve the correct space before the image or video loads, so it fills its box instead of pushing content down. It is the cleanest fix for responsive images that scale with their container.

How do web fonts cause layout shift and how do I stop it?

A web font usually loads after the page paints, so the browser shows a fallback font first and then reflows the text when the real font arrives. Cut the reflow by sizing the fallback to match: set font-display: optional, preload the critical font, and use the @font-face metric overrides (size-adjust, ascent-override, descent-override) so the two fonts take the same space.

Does CLS get worse under load?

Less than the other Vitals. Layout shift is mostly a property of page structure, an unsized image or a late ad slot, which behaves the same for one user or a thousand. Largest Contentful Paint and Interaction to Next Paint both degrade under concurrency because they depend on the server; CLS is the steadiest of the three.

How do I measure CLS for real users?

Read it in the field at the 75th percentile, not from a single lab run. Use Google web-vitals library (onCLS) to report the value from real sessions, or read the Chrome User Experience Report. The attribution build also names the largest shifting element, so you can see which component to fix.

See it on your site

Test in real browsers.
Debug in real sessions.

Want to see this measured on your app?

30 minutes. We build a scenario on your real customer journey, run a small test, and walk you through the report with your data in it.