What CLS is and how it scores
Cumulative Layout Shift is a measurement of how much the visible layout of a page moves around unexpectedly during load. The score is unitless. It is calculated as the largest shift "window" during the visit, where each shift contributes a score equal to the fraction of the viewport that moved multiplied by the distance the content travelled, expressed as a fraction of the viewport height. Pass at 0.1 or less at the 75th percentile of Core Web Vitals field data. See the Core Web Vitals explained chapter for the wider context.
Shifts caused by user interaction within 500 milliseconds are explicitly excluded. Clicking a button that expands a card is not a layout shift. Tapping a hamburger that opens a menu is not a layout shift. CLS only counts the unexpected ones: the cookie banner that appears at the top of the screen 800ms after first paint and pushes the headline three screens down. That is what the metric is designed to catch.
Of the three Core Web Vitals, CLS is the cheapest to pass and the cheapest to fail. Most failing scores trace back to four code patterns, and each one has a five-line fix. The challenge is finding the shifting element, not fixing it once you have.
Step 1: Find the shifting element
You cannot fix CLS without first identifying which element is shifting and when. Lighthouse reports the top contributing shifts in its diagnostic section. The Performance panel in Chrome DevTools is more thorough.
- Open the page in Chrome with mobile emulation.
- Open DevTools, switch to the Performance panel.
- Throttle CPU to 4x and network to Slow 4G.
- Hit the record button and reload the page.
- Look at the Layout Shifts track in the resulting timeline. Each shift is a red bar with the element responsible labelled on hover.
The Web Vitals Chrome extension also displays the running CLS score and the shifting elements in real time. For a quick gut check on a single URL, it is the fastest tool available.
One subtlety: Lighthouse's reported CLS only captures shifts during the page-load phase. Real CrUX data captures shifts across the entire session. If your page has a slow-loading newsletter sign-up that appears 8 seconds after first paint, Lighthouse may not catch it but CrUX will. Test by leaving the page idle for 15 to 20 seconds while watching the Web Vitals extension.
Images without dimensions
The single most common cause of CLS, and the easiest to fix. An <img> tag without explicit width and height attributes leaves the browser guessing at the height. The browser renders the surrounding text first, then when the image finally arrives it pushes the text down to make room. Score contribution: large.
The fix is to set width and height attributes on every image:
<img src="hero.webp" width="1200" height="675" alt="...">
The numbers are intrinsic dimensions, not display dimensions. The browser uses the ratio to reserve the right aspect-ratio box before the image loads. The image still scales responsively via CSS (max-width: 100%; height: auto). Setting width and height does not lock the image to a fixed display size, it just tells the browser the aspect ratio in advance.
For responsive images using srcset, set width and height to the dimensions of the default src image. The browser uses the ratio, not the absolute pixel count.
For CSS-positioned images (background images, CSS art), use the aspect-ratio CSS property on the container:
.hero { aspect-ratio: 16 / 9; background-image: url(hero.webp); }
Every modern browser supports aspect-ratio (Chrome 88+, Firefox 89+, Safari 15+). It has been broadly safe to use since late 2021. The image SEO chapter covers the wider image workflow.
Embeds, iframes and ad slots
YouTube embeds, Instagram embeds, TikTok embeds, Google Maps embeds, ad slots from AdSense or a header-bidding wrapper. All of these resize after their content arrives. Without reserved space they cause large layout shifts.
The fix is to set a minimum height on the embed container in CSS:
.youtube-embed { min-height: 360px; aspect-ratio: 16 / 9; }
Same for ad slots:
.ad-slot { min-height: 280px; }
If the ad sometimes loads a 250px-tall creative and sometimes a 90px-tall creative, reserve the larger height. A bit of empty space below a short ad is a far better outcome than a 160px layout shift on every page load.
For YouTube specifically, consider lite-youtube-embed or similar facade components. These render a click-to-load thumbnail at the correct aspect ratio, then load the iframe on interaction. Saves a tonne of JavaScript and avoids any CLS contribution from the embed itself.
For Google Maps embeds, the iframe has explicit width and height attributes baked into the embed code Google gives you. Use them. If you wrap the iframe in a responsive container, set aspect-ratio on the wrapper.
Web font swaps and FOIT/FOUT
Web fonts cause CLS when the swap from the system fallback font to the custom font happens after first paint and the two fonts have meaningfully different metrics. The text reflows, and any content below the text reflows with it.
Three patterns to address this, in order of safety:
Self-host the font and preload it. A self-hosted WOFF2 with a preload hint usually arrives before first paint, so the swap happens before the user sees anything and no shift is recorded. Add a <link rel="preload" as="font" type="font/woff2" crossorigin> in the document head for the critical font weight.
Use a size-adjusted fallback. CSS size-adjust, ascent-override, descent-override and line-gap-override let you adjust the metrics of the fallback font so they more closely match the custom font. When the swap finally happens, no reflow occurs because the line height did not change. Tools like Fontaine and the Capsize calculator generate the right overrides for any font pair.
Use font-display: optional. This tells the browser to only use the custom font if it arrives within 100ms. Otherwise the page renders in the fallback for the entire session. Zero font-swap CLS, but you sacrifice the custom font on slow connections. Suitable for some brands, unacceptable for others.
Avoid font-display: swap with no other mitigations. The default swap behaviour is the worst-case CLS pattern: fallback font renders, custom font arrives 600ms later, layout reflows. Either use the size-adjusted fallback approach or pair swap with a preload hint.
Content injected above the fold
Cookie banners, announcement bars, GDPR consent dialogs, newsletter sign-up bars, ad banners injected after JavaScript runs. Each of these can push the entire page down by 60 to 200 pixels, generating a huge CLS contribution.
Three approaches that all work:
Use overlays instead of inserted elements. A cookie banner positioned with position: fixed at the bottom of the viewport does not move any other content. CLS contribution: zero. This is the cleanest fix and the one most major sites have adopted post-2021.
Reserve space in CSS. If you must insert the banner inline, reserve a placeholder element in the HTML and CSS that has the same dimensions as the banner. JavaScript replaces the placeholder content but does not resize the container. No shift occurs.
Render server-side. Cookie banners rendered as part of the initial HTML response do not cause shifts because they are present from first paint. The cost is some plumbing to know consent state server-side, which most modern consent platforms support.
For announcement bars and promotional banners that change on a schedule, server-side rendering is usually the right call. Promotional bars that change with the user (logged-in versus logged-out, country-specific offers) can use the overlay or placeholder approach.
Do
- Set width and height on every
<img>tag - Use
aspect-ratiofor CSS-positioned images and embeds - Reserve a minimum height for every ad slot and embed
- Preload critical web fonts
- Use overlays or reserved placeholders for cookie banners
Don't
- Ship images without dimension attributes
- Use bare
font-display: swapwith no fallback adjustment - Inject content above existing content via JavaScript
- Use unframed YouTube embeds without aspect-ratio reservations
- Trust Lighthouse CLS alone (it misses late-session shifts)
Common mistakes
Trusting Lighthouse over field data. Lighthouse's CLS measurement covers the page-load phase only. CrUX measures shifts across the entire session. A page can score 0.02 in Lighthouse and 0.18 in CrUX because of a delayed newsletter pop-up that the lab never saw. Always cross-check the GSC Core Web Vitals report against the lab number.
Forgetting the mobile menu drawer. A hamburger that opens a drawer is fine. A hamburger that opens an inline accordion that pushes the page down is a shift. If the menu must push the page (rare requirement), exclude it from the CLS calculation by adding the open/close interaction within 500ms of the click, which qualifies as user-initiated.
Ignoring late shifts. Many sites pass CLS in Lighthouse and fail in CrUX because of shifts triggered by lazy-loaded content far down the page, by infinite-scroll loaders, or by widgets that resize as users interact. Scroll through the entire page during testing, not just the initial viewport.
Treating CWV as a tiebreaker boost. CLS is part of the small confirmed Page Experience signal. Pass the threshold, then move on. Great content with weak CLS beats weak content with great CLS.
Tools and checklist
Free tools for the full CLS workflow:
- Lighthouse. The Performance audit reports CLS plus the top three shift contributors with element selectors.
- Chrome DevTools Performance panel. The Layout Shifts track shows every shift in chronological order with the element responsible.
- Web Vitals Chrome extension. Live CLS readings as you scroll and interact with the page. Catches late-session shifts that Lighthouse misses.
- PageSpeed Insights. Both field and lab CLS in one view.
- GSC Core Web Vitals report. The official field-data view, URL-group level.
Pre-launch checklist:
- Every
<img>has explicit width and height attributes - Every embed and iframe has a reserved aspect-ratio or min-height
- Every ad slot has a reserved minimum height
- Critical fonts are self-hosted with a preload hint
- Cookie banners are overlays or server-rendered, not JS-injected above content
- Lazy-loaded content below the fold has reserved dimensions
- Mobile and desktop both tested with full-page scroll-through
- Field data tracked in GSC for 28 days post-fix
Perth and WA context
Two CLS patterns specific to Perth and WA sites.
Tradie and trades sites are the worst offenders for cookie-banner CLS. A typical Perth electrician or plumber site built on a budget WordPress theme will ship with a generic cookie banner plugin that injects above the header on every page. Mobile users see the headline jump down 80 pixels half a second after first paint. The fix is one of three things: kill the plugin, switch to an overlay-style banner, or reserve space in CSS. Trades SEO sites we audit usually fix CLS in less than an hour once the banner is identified.
Real estate and e-commerce sites struggle with embed CLS. Property gallery widgets, virtual tour embeds, third-party booking widgets, review carousels. All of these are notorious for shipping without dimensions. Real estate SEO sites and e-commerce SEO sites benefit most from a strict policy of reserved space for every embed.
For broader context, the speed optimisation service handles CLS work alongside LCP and INP. New sites should start with a free SEO audit. For locations, see Perth services, Local SEO Perth, Mandurah SEO, and Rockingham SEO. For the rest of the Vitals work, see LCP optimisation, INP explained, and Lighthouse explained. The technical SEO pillar covers the wider hygiene context.