Every CLS fix is the same move: reserve the space before the content arrives. The fix is not the hard part. Finding the shift is, because the tool almost everyone opens first stops watching before the shift happens.
Lighthouse loads the page and stops. Your visitors keep scrolling. CLS in the field counts everything that moves for the whole life of the page, which means a WordPress site can report a perfect 0 in the lab and fail in Search Console — and the two numbers are both correct. Sort that out first and the rest of this is a list of six things to reserve space for.

A CLS of 0 in Lighthouse proves nothing
Cumulative Layout Shift passes at 0.1 or less, at the 75th percentile of page views, split between mobile and desktop. Above 0.25 is poor (web.dev, verified August 2026). Those are the same rules as the other vitals. What is not the same is what the lab can see.
Google defines CLS as the largest burst of layout shift scores “that occurs during the entire lifecycle of a page”. A lab test does not have an entire lifecycle. It has a load. web.dev is direct about the consequence: “CLS measured in the lab only considers layout shifts that occur above the fold and during load, but this is only a subset of what CLS actually measures”, and “CLS values reported by lab tools for a given page may be less than what real users experience in the field” (verified August 2026).
This is the one Core Web Vital where the lab test is structurally blind to the most common cause. For LCP, a lab test at least measures the same event as the field, just on a different device. For CLS it measures a shorter event. Every shift that fires when someone scrolls past a lazy-loaded image, or two seconds after load when the consent script finishes, or when a “related posts” block finally renders, is invisible to it. web.dev names the pattern exactly: pages commonly “lazy-load images or iframes without dimensions, and that can cause layout shifts when a user scrolls to those sections of the page. But those shifts may only happen if the user scrolls down, which often won’t be caught in a lab test.”
So the familiar scene — a perfect lab score screenshotted and pinned to a report, sitting next to a field score that keeps failing — is not a contradiction and not a bug in Google’s tools. The test never scrolled. The visitors did.

How a shift is scored, and why one banner beats fifty nudges
A single layout shift is scored as impact fraction × distance fraction. The impact fraction is the share of the viewport covered by the unstable elements, counting where they were and where they ended up; the distance fraction is the greatest distance any of them moved, divided by the viewport’s largest dimension (web.dev, verified August 2026). Both are proportions of the screen, so both punish the same thing: something big moving a long way.
That arithmetic decides what you fix. A consent bar that appears after load and pushes down content filling three quarters of the screen, by a fifth of the screen height, clears 0.15 on its own — a failing grade from one element on one page. Fifty nudges of a few pixels each score close to nothing between them. Rank the work by area times distance, not by how many shifts the tooling lists.
The second rule that changes priorities is the session window: a burst of shifts less than a second apart, up to five seconds long, and CLS reports the worst window rather than the sum (web.dev, verified August 2026). Your score is one bad moment, not a running total. Fixing that one moment can drop the whole metric, and a page with a lot of scattered small shifts may already be fine.
The third rule is the escape hatch: shifts that happen within 500 milliseconds of a user interaction are not counted. An accordion that opens when someone clicks it is expected movement and costs nothing. A banner that arrives on its own schedule is not, and costs the full amount. When you cannot avoid moving the page, move it because the visitor asked.
The six things that move on a WordPress page
In practice the same six sources come up over and over on WordPress and WooCommerce sites. The fix column is the useful one — notice that not a single row says “load it faster”. Speed does not fix layout shift. Reserved space does.
| What moves | What the visitor sees | Where it comes from on WordPress | The fix |
|---|---|---|---|
| Images and videos without dimensions | Text jumps down the screen as each image paints, usually while scrolling | Images pasted into a Custom HTML block, hard-coded into a template, loaded from an external URL, or written by a page builder | width and height attributes on every image and iframe, or an aspect-ratio in CSS. Core does this for you on images it manages |
| Ads, embeds and iframes | A gap opens mid-article, or the whole page drops when the ad slot finally fills | Auto ads, YouTube and Maps embeds, Instagram and X cards, third-party review widgets | A container with a fixed min-height sized on the most common slot size. Never let the third party size the box |
| The cookie or consent banner | Everything moves down a second or two after the page looked ready | Consent plugins that inject the bar via JavaScript after load; also promo bars and shipping notices | Render it as a fixed overlay on top of the page, or output it in the initial HTML with its height reserved |
| Web fonts | Text paints in the fallback, then reflows into different letter widths | Theme or plugin loading Google Fonts, icon fonts, a builder with its own font stack | Self-host and preload, match the fallback metrics with size-adjust and ascent-override, or use font-display: optional |
| Content injected by JavaScript | A block appears above what you were reading and pushes it off screen | Sliders built after load, related-posts and upsell blocks, A/B test variants, sticky headers that engage on scroll | Render it server-side, place it below the fold, or reserve the exact space it will occupy |
| Animations on layout properties | Elements creep as you scroll past them | Theme and page-builder entrance effects animating height, top or margin | Animate transform instead — composited animations using translate cannot move other elements |
On the ads row, web.dev does not hedge: “Ads are one of the largest contributors to layout shifts on the web” (verified August 2026). If your site carries ad slots, start there and the rest of the table can wait a week.

What WordPress already does, and where it stops
Core has been handling the image row since 2020. WordPress 5.5 “start[ed] back-filling width and height attributes on img tags when they are not already present”, so the browser can “infer the page layout ahead of actually loading the image” (Make WordPress Core, July 2020, verified August 2026). The same release tied lazy loading to that guarantee: “By default, WordPress will add loading=“lazy” to all img tags that have width and height attributes present.”
Read that rule the other way round and it is quietly clever: core refuses to defer an image whose space it cannot reserve. Lazy loading and known dimensions ship together or not at all.
Three things break it, and all three are things you installed:
- Markup core never sees. An image in a Custom HTML block, written directly into a template, served from another domain, or generated by a page builder with its own renderer does not go through the attachment pipeline, so nothing back-fills it.
- Plugins that lazy-load everything. An optimisation plugin that applies lazy loading across the board, including iframes and background images, will happily defer the elements core left alone precisely because their size was unknown.
- Everything that is not an image. Core has no opinion about your consent banner, your ad slots, your web fonts or your slider. Five of the six rows in the table above are entirely yours.
One thing that is not a problem, despite how often it gets blamed: the responsive CSS pattern img { width: 100%; height: auto; }. Modern browsers derive an image’s default aspect ratio from its width and height attributes, so that CSS plus those attributes is the recommended combination, not a conflict (web.dev, verified August 2026). Stripping the attributes to “let CSS handle it” is what causes the shift.
How to actually find your shifts
Given that the default tool is the blind one, it matters which window you look through. Each of these sees a different slice, and the mistake is treating any single one as the verdict.
| Where you look | What it sees | What it misses | Use it for |
|---|---|---|---|
| Lighthouse / PageSpeed Insights lab score | Shifts above the fold, during load, on a simulated device | Everything after load and everything below the fold | A quick check that you have not made load worse. Not a verdict |
| PageSpeed Insights field data (CrUX) | Real Chrome visitors, whole page lifecycle, 28-day window | Which element moved, and on which page template | Deciding whether you actually have a problem |
| Search Console, Core Web Vitals report | The same field data grouped by URL pattern, mobile and desktop apart | Anything younger than about four weeks | Finding which page type is failing, and confirming a fix landed |
| Chrome DevTools, Performance panel | Each individual shift, with the offending element highlighted | Whatever you did not do while recording | Naming the exact element once you know a page is bad |
| DevTools rendering, Layout Shift Regions | A live blue flash over anything that moves, while you use the page | Nothing on that page — you have to drive it yourself | Scrolling the whole page slowly on a throttled connection. The most useful two minutes here |
The last row is the one to internalise. Turn on Layout Shift Regions, throttle the network to a slow connection, then scroll from the top of the page to the bottom at reading speed and watch where it flashes blue. That is the test the lab tools are not running, and it takes about as long as reading this paragraph twice.
What to do, in order
- Get the field number first. Search Console, Core Web Vitals, mobile and desktop separately. If you pass at 0.1, you are done — go and read the LCP guide instead.
- Reproduce it with Layout Shift Regions on, throttled, scrolling the whole page. Note what flashes and how far it moves.
- Deal with the consent banner and the ad slots. Largest area, largest distance, and they fire after load where nothing else was watching. Usually most of the score.
- Audit images and iframes for dimensions — in the page source, not in the media library. Look specifically at anything a builder or a Custom HTML block produced.
- Check what your optimisation plugin is lazy-loading. Anything without known dimensions should come off the list.
- Then fonts. Self-host, preload, and adjust the fallback metrics so the swap does not change the number of lines a paragraph takes.
- Re-record locally to confirm the shift is gone, then leave Search Console for four weeks. Its window is 28 days, so it cannot tell you anything useful sooner.
When it is not worth chasing
If your field CLS is already under 0.1, stop. Going from 0.06 to 0.01 changes no classification and no visitor’s experience. Core Web Vitals are a ranking signal of modest weight, and CLS is the one where the marginal gain is smallest, because below the threshold nobody perceives the movement at all. Spend the afternoon on the vital you are failing.
And do not fix CLS by making the page slower. This one is common enough to be worth naming. Someone discovers that hiding the whole body until every asset has loaded produces a beautiful 0.00, ships it, and the site now shows a blank screen for two seconds before anything appears. The same trick with a longer font-display block window, or a full-page loading spinner, has the same shape: a metric traded for a worse experience. CLS exists as a proxy for “the page did not move under my thumb”. A page that shows nothing does not move, and it also does not work.
Where the work does pay: a site failing CLS in Search Console on mobile, a shop where the price or the Add to cart button moves after the page looks settled, or any template where a consent banner and an ad slot fire in the same second. That is part of what our WordPress speed optimisation work covers, measured on the field numbers rather than a lab screenshot. On a store the same reasoning extends to the pages the cache cannot help with, which is why a WooCommerce buyer gets the slowest version of the shop.
Frequently asked questions
What is a good CLS score?
0.1 or less, measured at the 75th percentile of page loads and split between mobile and desktop. Above 0.25 is poor, and everything in between needs improvement (web.dev, verified August 2026). The percentile is the part people skip: a quarter of your visitors can be worse than your reported score, so a single clean test on your own machine says nothing about whether the page passes.
Why does Lighthouse report a CLS of 0 when Search Console says the page fails?
Because the two are measuring different lengths of time. Lighthouse loads the page in a synthetic environment and stops, so it only sees shifts that happen above the fold during load. Field data covers the entire lifecycle of the page, including everything that moves as the visitor scrolls. web.dev states it plainly: CLS measured in the lab only considers layout shifts that occur above the fold and during load, which is only a subset of what CLS actually measures. A lab score of 0 is not evidence that the page is stable.
Does adding width and height to images fix CLS in WordPress?
It fixes the image half of the problem, and WordPress has been doing it for you since version 5.5, which back-fills width and height attributes on img tags that do not already have them. What it cannot cover is markup it never touches: images pasted into a Custom HTML block, hard-coded into a theme template, pulled from an external URL, or built by a page builder that writes its own markup. And images are only one of six common sources — a site can have perfect image dimensions and still fail on a cookie banner.
Is my cookie banner hurting CLS?
If it is injected by JavaScript after the page has painted and it pushes the content down, yes, and usually more than everything else combined, because it moves a large share of the viewport by a large distance. Shifts within 500 milliseconds of a user interaction are excluded from CLS, but a banner that appears on its own is not a response to anything the visitor did. The fix is not removing the banner. It is showing it as a fixed overlay that sits on top of the page, or reserving its height in the initial HTML.
Do lazy-loaded images cause layout shift?
Only when they have no dimensions, in which case the shift happens as the visitor scrolls them into view — the exact scenario a lab test never reaches. WordPress core avoids this by only adding loading="lazy" to images that already have both width and height attributes present. The risk comes from optimisation plugins that lazy-load everything on the page regardless, including images and iframes core deliberately left alone.
How long before Search Console shows the improvement?
Roughly four weeks. The Core Web Vitals report is built on field data aggregated over a rolling 28-day window (Search Console Help, verified August 2026), so the day after you deploy a fix, 27 of the 28 days still contain the old behaviour. Check the lab in DevTools for immediate confirmation that the shift is gone, then leave Search Console alone for a month before deciding whether it worked.
Sources (verified August 2026): web.dev — Cumulative Layout Shift (CLS); web.dev — Optimize Cumulative Layout Shift; web.dev — Why lab and field data can be different; Make WordPress Core — Lazy-loading images in 5.5; Search Console Help — Core Web Vitals report.