Your site’s quality scores in PageSpeed Insights and Google Search Console jump between visits because of one metric - CLS (Cumulative Layout Shift). Fixing CLS mostly comes down to one principle: the browser needs to know the size and position of every element in advance, so nothing moves after the user has already seen it. Below: the concrete causes of layout shifts, working CSS fixes, and how to check exactly what’s “jumping” on a given page.
According to CrUX data for May 2026, cited in a review by DigitalApplied, CLS is the Core Web Vitals metric with the highest pass rate: 81.3% of sites stay within the 0.1 threshold, versus 68.6% for LCP and 86.6% for INP. That creates a false sense of security - since CLS “usually passes anyway,” it’s the first thing teams forget about when PageSpeed shows an unstable score from visit to visit on the same page.
In our site audit projects, we see the same pattern over and over: a client sends a PageSpeed screenshot with a score of 92, then the next day it’s 61 for the same page with zero code changes. The first reaction is usually “the server is slow” or “Google is glitching.” In reality, it’s almost never speed - it’s the instability of one particular element, which shifts differently depending on what did or didn’t finish loading before it.
Next, we’ll go through exactly which elements cause the shift, how to lock them down in CSS without changing the design, and how to tell a real layout problem apart from random measurement noise.
For a business, this isn’t an abstract metric in a report. Every layout shift is a split second where the user is aiming for one button, and a different one ends up under the cursor: a subscription banner instead of the “Buy” button, an ad instead of a link to the article. A mis-click is annoying and looks unprofessional, and for an online store or a landing page with checkout, it’s a direct loss of conversions, not just search rankings.
CLS (Cumulative Layout Shift): a Core Web Vitals metric that sums up every unexpected shift of visible page elements over the page’s lifetime. A value below 0.1 is considered good, 0.1 to 0.25 needs improvement, and above 0.25 is poor.
How to measure the shift before you fix CLS
Before you fix a shift, you need to know exactly which element is causing it - and here it’s important not to confuse field data with lab data.
Field data (CrUX) is collected from real Chrome traffic over a rolling 28-day window - this is what Google Search Console shows in its Core Web Vitals section, and it’s what affects ranking. Lighthouse in PageSpeed Insights or DevTools is a lab test: a single synthetic run under fixed conditions, which may not match what real users see on different devices and connections.
That difference explains the jumping score: if PageSpeed shows 92 on one run and 61 on another, the lab test is most likely catching a different moment each time when something on the page shifts - an ad, a banner, a font. CrUX field data smooths this out across thousands of visits, but that’s exactly why the real picture doesn’t show up right away - it takes weeks.
To debug a specific shift, open the Performance tab in Chrome DevTools, record a page load, and look for Layout Shift entries on the Experience track. Clicking an entry shows exactly which element shifted, by how many pixels, and at what moment - it’s faster than guessing from the code.
How to find the problem element with DevTools
Open DevTools (F12) -> Performance tab -> Record. Reload the page, wait for it to finish loading, then stop the recording. On the Experience track, find the red Layout Shift blocks - clicking a block expands a Summary showing which element shifted and by how much at that specific moment.
A second option is Google’s Web Vitals extension, or the web-vitals library in production: it sends real CLS values from users’ devices to your analytics, including a reference to the specific DOM element.
Images, ads, and widgets with no reserved space
The most common cause of high CLS is an element whose final size the browser doesn’t know until the content finishes loading.
If an <img> tag doesn’t have width and height set (or the CSS aspect-ratio property), the browser can’t calculate the block’s height in advance and leaves it at zero until the file finishes loading. When the image arrives, everything below it shifts down by exactly its height. The same happens with video, iframe embeds (maps, YouTube videos, booking widgets), and ad blocks.
Example: explicit dimensions and aspect-ratio
<img src="/hero.webp" width="1200" height="630" alt="Description">
.embed-container {
aspect-ratio: 16 / 9;
width: 100%;
}
Both approaches solve the same problem - the browser reserves space for the element as soon as it parses the HTML, before the file downloads. aspect-ratio is more convenient for responsive layouts, where the width changes across breakpoints and the height needs to scale proportionally.
The image’s own weight and format (AVIF versus WebP, srcset, load priority) is a separate topic that affects LCP, not CLS, and is covered in detail in the article on LCP optimization through images. For CLS, only the reserved container dimensions matter, not the file weight.
Fonts that shift text when they load
Web fonts cause a shift not because they’re heavy, but because the browser has to repaint the text once, when the font finally loads.
FOIT (Flash of Invisible Text) - the browser hides the text completely until the custom font downloads, showing blank space instead. FOUT (Flash of Unstyled Text) - the browser shows the text in a system font right away, then swaps it for the custom one. In both cases, if the system and custom fonts have different character widths, the swap shifts everything that comes after the text - paragraphs, buttons, neighboring blocks.
The font-display CSS property controls this behavior. The swap value shows the text in the system font immediately, but doesn’t eliminate the shift when the swap happens. The optional value is the choice for layout stability: if the custom font doesn’t load within the first 100 milliseconds, the browser shows only the fallback font for the rest of the session, with no swap and no shift.
Example: font-display: optional with preload
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom.woff2") format("woff2");
font-display: optional;
}
<link rel="preload" as="font" href="/fonts/custom.woff2" type="font/woff2" crossorigin>
Combining preload with font-display: optional is the most reliable way to guarantee no shift from fonts: either the font loads in time for the first paint, or the page stays on the fallback font for the whole session.
If completely dropping the custom font on a slow connection isn’t acceptable for the brand, a second option is to pick a fallback font with similar character-width metrics using size-adjust and related @font-face descriptors, so the width difference after the swap is visually unnoticeable.
Content that gets inserted above what’s already on screen
A separate category of shifts isn’t about an element’s size, but about the moment it appears: if a new block gets inserted via JavaScript above content that’s already rendered, everything below it gets pushed down.
The classic example is a cookie consent banner that appears a second after the page loads and pushes the site header down. A similar situation is an ad block that loads asynchronously and gets inserted near the top of the page, or a promo notification above the main content.
A working approach is to reserve space for such a block in advance with CSS, even before the content itself is ready: an empty container with a fixed min-height keeps the layout stable, and once the content loads it just fills the space that’s already there, without pushing anything. A second option for banners and cookie notices is to position them over the content using position: fixed or position: sticky at the bottom of the screen instead of embedding them in the document flow - that way they physically can’t shift the rest of the layout.
Animations that move the layout, not just move visually
Not every animation counts toward CLS - the difference is in which CSS property drives it.
Animations driven by top, left, width, height, or margin force the browser to recalculate the position and size of elements in the document flow on every frame - which is exactly what CLS registers as a shift. Animations driven by transform and opacity are painted by the browser on a separate compositor layer, without touching the document layout at all - visually the element moves the same way, but it doesn’t count toward CLS.
Example: animating with transform instead of top
/* Bad: animating with top recalculates layout every frame */
.banner {
position: relative;
animation: slide-in-bad 0.3s ease-out;
}
@keyframes slide-in-bad {
from { top: -100px; }
to { top: 0; }
}
/* Better: the same visual effect using transform */
.banner {
animation: slide-in-good 0.3s ease-out;
}
@keyframes slide-in-good {
from { transform: translateY(-100px); }
to { transform: translateY(0); }
}
An important detail of the methodology: shifts that happen within 500 milliseconds of a user’s click, tap, or key press don’t count toward CLS - Google treats them as an expected response to an action, not an unexpected shift. Scrolling and pinch gestures don’t fall under this exception, though: an animation that shifts elements during a scroll using layout properties adds to the CLS score just like any other shift.
How CLS dropped from 0.34 to 0.03 on a real project
In a typical audit project for a landing page with a lead form, PageSpeed showed an unstable result: anywhere from 88 to 54 across different runs of the same page, while LCP was generally fast and INP was good.
The cause turned out to be three independent sources of shift, all at once:
- A discount promo banner loaded via JavaScript 800 milliseconds in and got inserted above the hero block, pushing the whole page down
- The hero image had no
widthandheightattributes - The custom heading font was loaded without
font-display, so the browser defaulted toblock- the text repainted with a noticeable difference in character width
What changed over a few days of work: reserved a fixed-height container for the promo banner before it even loaded, set width and height on every above-the-fold image, and added font-display: optional with preload for the heading font.
The result, based on CrUX field data 28 days after deployment:
- CLS: 0.34 (poor) -> 0.03 (good)
- PageSpeed stabilized in the 90-94 range across all runs, instead of swinging between 54 and 88
- LCP and INP didn’t change, confirming that the problem was specifically about layout stability, not speed
Who this matters most for
Unstable CLS shows up most often on sites where content beyond the core layout loads dynamically: online stores with ad blocks and promo banners, landing pages with cookie notices and signup forms, and WordPress or Tilda sites with multiple plugins, each adding its own script independently of the others.
A separate category is companies that recently switched their WordPress theme, added a new slider plugin, or connected a chat widget: any of these changes can quietly push CLS back into the red zone, even if the metric used to be fine.
If your site runs on WordPress, platform-specific causes - plugin conflicts, Elementor, Gutenberg blocks without dimensions - are covered separately in the article on Core Web Vitals on WordPress. This article focused on causes that are common to any stack: WordPress, Astro, Next.js, or hand-built markup. The third Core Web Vitals metric - responsiveness to user actions - is covered in detail in the article on INP optimization.
A one-time fix removes the symptom, but a new plugin, a marketing banner, or a theme update can easily bring CLS right back. For sites that change often, this is worth monitoring on an ongoing basis as part of site support, not checking once a year.
Frequently asked questions
Why does PageSpeed show different scores for the same page?
This is most often a sign of unstable CLS: if an ad, banner, or font shifts the layout at different times across different runs, the Lighthouse lab test catches a different moment of the shift each time and returns a different score. A stable score with no variance is a good indirect sign that there are no shifts on the page at all, not just a small CLS value.
Does an animation count against CLS if the element shifts before the user actually sees it?
Yes, CLS counts shifts of visible elements regardless of whether the user physically noticed them - the metric measures the screen area that changed, not the fact of perception. The exception is shifts that happen within 500 milliseconds of a click, tap, or key press: those are treated as an expected interface response and don’t count toward CLS.
What do you do about a cookie banner or ad that you can’t skip showing?
You can and should show them - the problem isn’t the banner itself, it’s how it’s embedded in the layout. If you position the banner over the content with position: fixed at the bottom of the screen instead of inserting it into the normal document flow, it won’t physically push the rest of the page around and won’t count toward CLS.
How long do you have to wait to see the fix reflected in your reports?
Lighthouse and PageSpeed Insights show the effect right after deployment, since they’re a lab test of a single run. CrUX field data in Google Search Console updates on a rolling 28-day window, so it’s worth checking the full picture for real users about a month after the changes go live.
Are the causes of CLS different on WordPress versus a headless stack like Astro or Next.js?
The underlying causes are the same everywhere - CLS doesn’t depend on how the HTML gets rendered, it depends on whether space is reserved for elements in advance. On WordPress, the source is most often the theme, the page builder, or a conflict between several plugins that independently insert scripts. On a headless stack like Astro or Next.js, there are fewer hidden sources like that, because the markup is fully under the team’s control, but the mistake is the same one: a missing width and height on an image, or an animation using top instead of transform.
What to check first:
- Open the Performance tab in Chrome DevTools and find the actual
Layout Shiftentries, instead of guessing from how the page looks - Set
widthandheight(oraspect-ratio) on every above-the-fold image, video, and iframe embed - Add
font-display: optionalwithpreloadfor custom fonts that change the text after loading - Check whether dynamic content - banners, ads, cookie notices - is being inserted on top of a page that’s already rendered
If PageSpeed on your site jumps between runs and real users are complaining about an unstable interface - describe the problem to the Exceltic.dev team. We’ll find the specific element shifting the layout and propose a fix without changing the design.