What is Cumulative Layout Shift, and what does the CLS score measure?
CLS, short for Cumulative Layout Shift, is a score for how much the visible parts of a page move without the reader asking them to. You go to tap a link, a banner drops in above it, and you tap the wrong thing. That's a layout shift, and CLS is the number that adds those moments up. It's one of the three Core Web Vitals, and it's always in scope when I give help passing Core Web Vitals on WordPress.
Each shift gets its own score first: impact fraction times distance fraction. The impact fraction is the share of the viewport the moving elements cover across the two frames. The distance fraction is the furthest any of them moved, as a share of the viewport's larger side. In web.dev's worked example, an element covering 75% of the viewport across both frames drops by a quarter of the viewport's height, so the shift scores 0.75 times 0.25, which is 0.1875. One shift like that is already past the 0.1 line.
The adding up isn't a simple total. Google's definition groups shifts into bursts it calls session windows: shifts less than a second apart join the same window, and a window can't run past five seconds. The page's CLS is the burst with the highest score, measured across the whole life of the page, not only while it loads. So a page that shifts a little at load and a lot when the reader scrolls is scored on the scroll.
Three things don't count, and knowing them saves a lot of wasted fixing. A shift within 500 milliseconds of a click, tap or keypress is treated as expected, because the reader caused it. Scrolling and pinching don't earn that pass. Adding a new element doesn't count on its own either, unless it pushes something that's already visible. And an animation built on CSS transform moves an element without moving anything around it, so it doesn't shift the layout at all.
| Rating | CLS score | What it means for the URL group |
|---|---|---|
| Good | 0.1 or less | Passes the CLS part of Core Web Vitals |
| Needs improvement | Over 0.1, up to 0.25 | Doesn't pass, and it's usually one or two elements |
| Poor | Over 0.25 | Fails, and in Search Console one poor metric sets the whole group's status |
How do I see which element is shifting?
Most advice on how to fix CLS starts with a list of fixes. I start with a name. Every change I make is aimed at one element I've watched move, and these steps are how I get it.
- Step 01
Start from the failing group, not the homepage
Search Console's Core Web Vitals report groups similar pages together, so I open the group that's failing on CLS and pick a URL from it. The homepage is often fine while every blog post or product page fails, because they share a template the homepage doesn't use.
- Step 02
Turn on Layout Shift Regions
In Chrome DevTools I open the Rendering tab, tick Layout Shift Regions and reload. Anything that shifts flashes purple. It's crude, but it shows me in seconds whether the jump is in the header, the hero, the body copy or something injected at the bottom.
- Step 03
Record the load in the Performance panel
A recording puts every shift on its own Layout shifts track. Clicking one shows the shift animated and names the moved nodes in the Summary panel. I note when each one happens, because a shift at half a second and a shift at four seconds rarely share a cause.
- Step 04
Behave like a real visitor
I reload on a throttled mobile profile, scroll to the bottom, wait for the cookie notice, open the menu and let any chat widget load. Lab tools stop watching after the load, and the shifts that fail a group often arrive later than that.
- Step 05
Read the element list, then find the cause
The browser's Layout Instability API reports up to five moved elements per shift, with their position before and after. The element that moved is often an innocent paragraph. The cause is whatever grew above it, and that's the thing I go and fix.
Don't start by switching on every optimisation toggle in a speed plugin. Delaying scripts and swapping fonts can move a shift to a different moment without removing it, and then the name you needed is gone.
Which causes of Cumulative Layout Shift do I check first on WordPress?
This is the order I check them in on a WordPress site. It runs roughly from the cheapest fix to the fiddliest, and every one of them is a cause web.dev's own optimisation guide names.
- i.
Images without width and height
An image tag with no
widthandheighttakes no space until the file arrives, then shoves everything below it down. With both attributes set andheight: autoin the CSS, the browser works out the aspect ratio and reserves the box up front. Theme templates, builder image widgets and hand-pasted HTML are the places I look first. - ii.
Embeds and iframes with no reserved box
A video, a map or a booking form loads into a zero-height slot and then opens up. I give the wrapper an
aspect-ratioor amin-heightthat matches what arrives, so the space is held before the embed is. - iii.
Web fonts swapping in
When the real font replaces the fallback, the text reflows if the two don't share the same metrics. Preloading the main font, a closer fallback, or
font-display: optionaleach cut the jump. On builder themes it's often a font setting, and Divi's own Google Fonts option is a typical example of where it hides. - iv.
Cookie banners and notice bars pushed in at the top
A consent banner or promo bar injected above the header moves the whole page down after it's painted. I'd rather it overlay the page than push it, or have its space reserved from the first byte. Content that loads late belongs lower on the page, and it shouldn't arrive without the reader asking for it.
- v.
Ad slots and late widgets
Ads, related post blocks, review widgets and newsletter boxes that fill in after load are late content by design. I reserve the most likely height for each slot. An empty reserved gap is a better trade than a page that jumps.
- vi.
Animations and sliders that move the layout
Entrance animations and sliders that change
top, margins or height move what's around them. The same effect built ontransformdoesn't. Sliders that measure themselves with JavaScript after load are a regular source, and the builder's own settings usually decide it, which is why the wider speed work sometimes has to include a template change.
I fix one cause at a time and record again after each change. If the shift is still there, the element list from the last recording tells me what's left, and I haven't stacked five changes on a guess.
One more check helps real visitors who go back and forward: the back/forward cache. A page restored from it comes back exactly as it was left, without load shifts, and web.dev calls bfcache eligibility a highly effective way to keep CLS low. DevTools has a Back-forward Cache test under Application that names what's blocking it. On WordPress I look for plugins using the unload event, which web.dev says never to use, and Cache-Control: no-store on pages that don't need it.
Rather have me do this? One person, whole job.
Book a call on UpworkWhy can the lab score pass while field data fails?
Because they're watching different things. Lighthouse, the engine behind the lab half of PageSpeed Insights, loads the page once on a simulated device and can only see the shifts that happen during that initial load. Field data comes from real Chrome visits, it covers the whole life of the page, and PageSpeed Insights reports it over the previous 28 days.
So a page can score 0 in the lab and still fail. The lab run doesn't scroll, doesn't log in, doesn't see the personalised block a returning visitor gets, and never waits for the cookie notice a real visitor does. web.dev says as much: because lab tools only see the load, the CLS they report can be lower than what real visitors get.
| Tool | What it sees | What I use it for |
|---|---|---|
| Search Console Core Web Vitals report | Real visits, grouped by similar pages | Which template is failing, and whether a fix held |
| PageSpeed Insights, field section | Real visits to one URL over 28 days | The score that actually counts for that page |
| Lighthouse and the lab score | One simulated load, no scrolling or input | Catching load shifts, never judging a pass |
| DevTools Performance panel | My own session, shift by shift | Naming the element and timing each shift |
That's why I don't report a fix from a green lab score. The lab confirms I've removed the shift I could reproduce. The field data confirms real visitors stopped getting it.
For a cumulative layout shift test that doesn't wait on a rolling 28 days, web.dev points to its web-vitals JavaScript library. import { onCLS } from 'web-vitals'; onCLS(console.log); reports CLS the way the metric defines it, shifts after the load included, and the value can go to analytics instead of the console, so each template gets its own real-visit number.
How I confirm a CLS fix has held
Once the change is live, I clear any page cache and CDN cache so visitors get the fixed HTML, then run the same recording on the live URL. If the purple flash is gone and the Performance panel shows no shift from that element, the lab side is done.
Then I start validation in Search Console. Google runs a 28-day monitoring session for that issue and only marks it fixed if no URL on the site shows it during the window. I won't promise a date for that, because it depends on how much real traffic the group gets. And a group that doesn't get enough real visits drops out of the report altogether, so there's nothing to validate until the traffic's there.
While I wait, I watch the PageSpeed Insights field number for the busiest URLs. It moves slowly, but it moves well before the report catches up.
If Search Console says your CLS is failing and you'd rather one person found the element, fixed the cause and watched the field data until it passed, that's the Core Web Vitals work I do, remotely and in the order I set out before any change.
Book a call on Upwork
