The Core Web Vital that costs you money directly
A slow page annoys people. A shifting page charges them for the wrong thing.
Picture a product page on a phone. The buyer's thumb is already moving toward Add to Cart. A late offer banner injects above the fold, everything slides down 80 pixels, and the tap lands on "Buy now with UPI" instead — or on a recommended product they never wanted. Best case they go back and retry. Worst case you handle the return.
The same mechanic kills forms: a field shifts as a web font swaps in, the tap lands on the wrong input, and nothing in your analytics will ever label that as a layout problem.
That's why CLS is worth fixing even if you don't care about Core Web Vitals as a ranking input at all.
How the score is calculated, briefly
Each shift scores impact fraction times distance fraction — how much of the viewport moved, and how far it travelled as a share of the viewport. A full-width block moving a quarter of the screen scores far worse than a caption nudging ten pixels.
Shifts are grouped into session windows: a window opens at the first shift, extends while shifts keep coming within a second of each other, and caps at five seconds. Your CLS is the worst window, not the page total. One bad burst during load beats a hundred tiny ones.
Shifts within 500 milliseconds of a user interaction are excluded, on the reasonable basis that a menu you opened is supposed to move. Everything else counts.
- Good: 0.1 or below. Needs work: 0.1 to 0.25. Poor: above 0.25.
- Measured at the 75th percentile of real Chrome visits over a rolling 28-day window, split mobile and desktop.
- Chrome DevTools' Performance panel flags each shift and highlights the element that moved — that's your fix list, in order.
The four things that cause almost all of it
In practice CLS has a short list of causes, and they're the same list on nearly every site we audit.
| Cause | What happens | Fix |
|---|---|---|
| Images without dimensions | The browser reserves zero height, then reflows everything when the image arrives. | <img width="800" height="600"> — modern browsers derive the aspect ratio and hold the space. |
| Web fonts | Text renders in the fallback font, then reflows when the custom font loads (FOUT), or stays invisible then appears (FOIT). | Preload the font file, font-display: swap, and match the fallback's metrics with size-adjust. |
| Injected banners and ads | A cookie notice, offer strip or ad slot appears above existing content and shoves the page down. | Reserve the space with min-height, or overlay it as position: fixed so it takes no flow. |
| Late third-party widgets | Review stars, trust badges and chat bubbles render after everything else and expand their containers. | Give each widget a fixed-size wrapper before the script loads. |
The one-line fix, and the ones that take longer
Start with images, because it's genuinely one commit. Every <img> gets width and height attributes matching its intrinsic size. Browsers have used those to compute an aspect ratio and reserve the box since 2020, so the space is held before a byte of image data arrives. Responsive CSS still resizes it — set height: auto and the ratio holds. Where the pixel size is unknown but the ratio isn't, aspect-ratio: 16 / 9 on the container does the same job for embeds and iframes.
Then the injected elements, which is where the design conversation lives. A cookie bar or sale strip that pushes content down is a layout shift by construction. Either reserve its height in the initial HTML, even when it's empty, or render it as a fixed overlay that never joins the flow. A bar pinned to the bottom of the viewport is free; the same bar inserted at the top of <body> after two seconds is a 0.3 score.
Fonts last, and they're the smallest of the wins: preload the weight you use above the fold, set font-display: swap so text is readable immediately, and match the fallback metrics with size-adjust so the swap doesn't change line heights. If your whole template needs this treatment, it's Core Web Vitals work rather than a ticket.