// blog / speed

How I track down and fix Cumulative Layout Shift

Cumulative Layout Shift scores how much visible content jumps around unexpectedly on a page, and 0.1 or less is good. I find the element that moves in DevTools before I change anything. On WordPress it's usually images and embeds without dimensions, web fonts swapping, cookie banners, or ad and embed slots that load late.

By Muhammad Younus Updated 10 min read Speed

Speed and Core Web Vitals: illustration for "Tracking down Cumulative Layout Shift"
// 01 - the metric

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.

Google's CLS thresholds, judged at the 75th percentile of page visits on mobile and desktop
RatingCLS scoreWhat it means for the URL group
Good0.1 or lessPasses the CLS part of Core Web Vitals
Needs improvementOver 0.1, up to 0.25Doesn't pass, and it's usually one or two elements
PoorOver 0.25Fails, and in Search Console one poor metric sets the whole group's status
// 02 - finding it

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

// watch out

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.

// 03 - the usual suspects

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.

  1. i.

    Images without width and height

    An image tag with no width and height takes no space until the file arrives, then shoves everything below it down. With both attributes set and height: auto in 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.

  2. 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-ratio or a min-height that matches what arrives, so the space is held before the embed is.

  3. 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: optional each 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.

  4. 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.

  5. 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.

  6. 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 on transform doesn'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 Upwork
// 04 - field versus lab

Why 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.

What each tool sees, and what I use it for
ToolWhat it seesWhat I use it for
Search Console Core Web Vitals reportReal visits, grouped by similar pagesWhich template is failing, and whether a fix held
PageSpeed Insights, field sectionReal visits to one URL over 28 daysThe score that actually counts for that page
Lighthouse and the lab scoreOne simulated load, no scrolling or inputCatching load shifts, never judging a pass
DevTools Performance panelMy own session, shift by shiftNaming 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.

// 05 - after the fix

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.

// when to hand this over

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

Is this your site? Send me the URL.

Book a call on Upwork
// questions - speed

What else do people ask about this?

What is a good CLS score?

A CLS score of 0.1 or less is good. Over 0.1 up to 0.25 needs improvement, and over 0.25 is poor. Google judges it at the 75th percentile of real visits, so most visitors have to get a stable page, not just your own test.

Why is my CLS fine in Lighthouse but failing in Search Console?

Lighthouse only sees one simulated load, while Search Console uses real visits across the whole life of the page. Shifts from scrolling, cookie notices, logged-in content and late widgets show up in field data and never in the lab run.

Does a layout shift after I click something count toward CLS?

Not if it happens within 500 milliseconds of a click, tap or keypress. Google treats that as expected, because the visitor caused it. Scrolling doesn't get the same pass, so a shift while someone scrolls still counts.

How is CLS different from LCP?

LCP measures loading: when the largest image, text block or video in the viewport renders, and 2.5 seconds or less is good. CLS measures visual stability: how much that content moves. A page can pass one and fail the other, so Largest Contentful Paint gets its own diagnosis.

Can a speed plugin fix Cumulative Layout Shift on its own?

Sometimes, when the cause is something it controls, like font loading or adding image dimensions. It can't reserve space for an embed it doesn't know about, and it can't change how a builder animates a slider, so I name the element first.

// sources - checked

Where do these rules come from?

Platform and Google documentation, opened and read on the day this was written. Everything else is how I work.

  1. web.dev: Cumulative Layout Shift (CLS)
  2. web.dev: Optimize Cumulative Layout Shift
  3. web.dev: Debug layout shifts
  4. Chrome DevTools: analyze rendering performance with the Rendering tab
  5. Search Console Help: Core Web Vitals report
  6. PageSpeed Insights: about field and lab data
  7. web.dev: back/forward cache
  8. web.dev: Largest Contentful Paint (LCP)
Muhammad Younus, WordPress technical SEO consultant
// who wrote this

One consultant, writing down his own method

I'm Muhammad Younus, a WordPress technical SEO consultant with 8+ years of experience and 100+ client sites fixed. Top Rated Plus on Upwork with a 100% Job Success Score. Every post here is the way I actually do the job, written in the first person because it's one person doing it, remotely, with no team behind the name.

Nothing in these posts is a client I can't name or a number I can't show. Where a figure comes from a client's Search Console, it's from a published case study, and where a rule comes from Google or a platform, the source is linked at the bottom of the post.

// the service behind this post
Muhammad Younus with his arms folded and a relaxed, confident smile
// next step

Want this done on your own site?

Book a call on Upwork, send the URL and read-only Search Console access, and you'll get a written scope for the search side before any work starts. One person does the whole job, remotely.

Book a call on Upwork