Layout shift culprits in Lighthouse: how to fix them
Lighthouse 13 lists what moved while your page loaded and guesses why, sometimes wrongly. How to find the real cause, and the fix for images, ads, banners and fonts.
Does Layout shift culprits hurt my PageSpeed score?
Lighthouse lists the content that moved while your page loaded in the Layout shift culprits insight ("Layout shifts occur when elements move absent any user interaction"), called "Avoid large layout shifts" before Lighthouse 13. The insight scores nothing and turns red when CLS reaches 0.1, but CLS itself is 25% of your Performance score. Reserve the space for whatever loads late, so nothing below it moves.
Table of Contents!
What does the Layout shift culprits insight check?
Lighthouse's description says: "Layout shifts occur when elements move absent any user interaction. Investigate the causes of layout shifts, such as elements being added, removed, or their fonts changing as the page loads."
Lighthouse shows each burst of shifts on its own: the burst's Total first, then every shift in it with the element that moved, often a big block such as <main> that was pushed down. The grey line under a shift is Lighthouse's guess at what pushed it: "Unsized image element" or "Web font". When it has no guess, there is no line.
Under 0.1, where CLS is still good, the insight is grey, for information only.
On a recipe page I built like a WordPress food blog, with an ad under the header, a newsletter bar, a recipe card and a headline in a web font, Lighthouse found four shifts in one burst, 0.241 together.

Lighthouse 13.5.0, mobile, simulated throttling as in PageSpeed Insights. The featured image it names caused none of these shifts.
What causes a layout shift?
A layout shift happens when something takes its space after the content around it is already on screen: an image without a reserved height, an ad that fills an empty slot, a bar a script inserts above the header, a web font that is wider than its fallback, an animation of top, height or margin. Everything under it moves.
A shift's score is how much of the screen moved times how far it moved, so the same 50 pixels cost more on a phone, and a bar at the top costs more than a block near the bottom. Shifts less than a second apart add up into one burst, and the page's CLS is its worst burst (how CLS is scored).
Shifts in the half second after a click, tap or key press don't count. Scrolling is not such an input: content that moves while the visitor scrolls counts.

Before, first paint

Before, after 2 seconds

After, first paint

After, after 2 seconds
The recipe page on a phone. The dashed box is where the photo was at first paint. After the fixes, the bar, the ad and the headline's third line have their space from the first paint.
How much do layout shifts cost your Lighthouse score?
CLS on the recipe page on a phone, with one cause left in and the other three fixed, and with all four:
| Cause | CLS before | CLS after |
|---|---|---|
| Web font in the headline | 0.035 | 0 |
| Newsletter bar | 0.058 | 0 |
| Ad in an empty slot | 0.054 | 0 |
| Recipe photo without size | 0.036 | 0 |
| All four | 0.241 | 0 |
Each cause on its own stayed under 0.1, and Lighthouse showed the insight in grey. With all four it was red: the four shifts came within a second of each other and fell into one burst. So fix every shift in the burst, not only the largest.
The Performance score went from 87 to 100. FCP, LCP, Total Blocking Time and Speed Index didn't move, so all 13 points came from CLS.

Before

After
Lighthouse 13.5.0, mobile, simulated throttling.
How many sites fail CLS, and is it safe to ignore the insight?
89% of sites have a good CLS on phones in the Chrome UX Report. Sites with ads fail more often: 89% of sites without an ad network pass, 85% with one and 73% with two or more. Sites with a chat widget pass 82% of the time, and it differs per widget: sites with LiveChat 67%. Sites with a consent banner, a review widget, a video embed or an email signup tool pass about as often as all sites, so on its own a banner or widget like these is rarely the problem.
This is a correlation: sites that run ads differ from other sites in more than their ads. It does tell you where to look first (ad networks ranked by CLS). WordPress sites with an optimisation plugin pass as often as sites without one, so installing one won't fix your CLS.

Share of sites with a good CLS on phones (Chrome UX Report, 75th percentile at or below 0.1), all sites against sites that load each kind of third party. June 2026 crawl, 114,116 sites with phone data. Data: State of Web Vitals (corewebvitals.io).
Fix it when the insight is red or your field CLS on mobile is above 0.1: these fixes cost nothing in loading speed. Your field CLS is the real-user data at the top of PageSpeed Insights (Mobile tab) or Search Console's Core Web Vitals report. Leave the insight alone when it is grey and your field CLS is 0.1 or less at p75.
Why does Lighthouse show no layout shifts when Search Console says CLS is poor?
Lighthouse loads your page once, on a phone screen, and stops shortly after the load. It never scrolls or taps. Your visitors' CLS covers the whole visit: the ad in the middle of a post that fills when they scroll to it, a lazy-loaded image without a size that arrives just as it comes into view. Search Console and PageSpeed Insights give you that CLS without saying what moved.
CoreDash does. Set the device to mobile and group CLS by Attribution Element (CLS): you get the elements that moved for real visitors, each with its CLS.

CoreDash on an example recipe site (anonymised data), mobile, p75.
In this example the worst element is an ad slot inside the post (div.ad-slot.ad-incontent, 0.19), the ad that fills when the visitor scrolls to it. Last interaction (CLS), under More dimensions, tells you whether the shifts came after a scroll: here 0.17 after a scroll, against 0.09 without any interaction.

The same site by Last interaction (CLS).
How do you find the real cause when Lighthouse names the wrong element, or none?
The grey line is a guess from timing: Lighthouse names what loaded or painted at about the moment of the shift. On the recipe page it named the featured image, which has width="1200" height="675", under the font's, the bar's and the ad's shifts. None of its lines points at the bar or the ad.
Does the "Unsized image element" really have no size?
Under the grey line Lighthouse prints the image's thumbnail and its <img> tag. If the tag has no width and height, the image is unsized: go to the fix. If it has them, check in DevTools that CSS doesn't cancel them:
- Open the page in Chrome, open DevTools (F12, or Cmd+Option+I on a Mac), click "Select an element in the page to inspect it" (Ctrl+Shift+C; on a Mac, Cmd instead of Ctrl here and in the shortcuts below) and click the image from the thumbnail.
- Open the Computed tab in the sidebar and type
aspectin its Filter box. If it shows auto plus a ratio (auto 1200 / 675), or a ratio alone from CSS, the image is sized. If it shows only auto, no height is reserved, unless CSS gives the image a fixed width and height. - Type
widthin the Filter box of the Styles tab. A rule that setswidth: autoon the image cancels the ratio, even though Computed still shows it: then the image is unsized (the fix).

The image Lighthouse called unsized, in the Elements panel.
If the image passes these checks, it is sized and didn't cause the shift, whatever the grey line says. Look at the other line under the same shift (on the recipe page, "Web font"), or at what appeared at that moment.
What appeared at the moment of the shift?
- Open the Performance panel and click the gear, "Capture settings". Set Network to Slow 4G and CPU to 4× slowdown, and keep Screenshots ticked in the toolbar.
- Click "Record and reload" (Ctrl+Shift+E). The recording stops by itself. For shifts while scrolling, click Record (Ctrl+E) instead, scroll the page and click Stop.
- Hover a purple diamond in the Layout shifts track: the tooltip shows the shift's score and a screenshot of the page at that moment, with the moved area marked. Look just above the marked area: what is new there, or taller than before (a bar, an ad, a headline on one more line), caused the shift.
- Click the diamond for the Summary tab: Start time, Shift score, Elements shifted and Culprit. That Culprit is a guess too, but for an ad it can say "Injected iframe".

Chrome 155, Slow 4G and 4× CPU. Hovering the 0.0583 shift: the newsletter bar has just appeared above the header.
Block the suspect request and reload
- In the Network panel, right-click the request you suspect (the font file, the banner script, the ad tag) and choose Block requests, then Block request URL. Block request domain blocks every file of a third party.
- Reload: the request shows (blocked:devtools) in the Status column. Record again in the Performance panel. If a diamond is gone, that request caused it.
- Untick the pattern in the Request conditions drawer afterwards, or every page you open keeps the block.
How do you fix Layout shift culprits?
The grey line under a shift, or what you saw appear in the Performance panel, tells you which part applies.
Unsized image element: give the image its proportions
First check that the image really has no size (above). Then put its width and height on the <img> and let CSS scale it:
<img src="shakshuka-recipe-card-800x600.jpg" width="800" height="600" alt="Shakshuka with feta in a black pan">
img { max-width: 100%; height: auto; } The browser turns the two attributes into a ratio and reserves the right height at any screen width before the file arrives. The numbers don't have to be the display size, only the right proportions. aspect-ratio: 4 / 3 in CSS, with a width, does the same.
If a theme rule sets width: auto on images, remove it: it cancels the ratio, so the image takes no space until it loads, while Lighthouse's Diagnostic "Image elements do not have explicit width and height" still passes it (auto sizing images; video and responsive images in images and media).
- WordPress adds width and height to images from the media library. Check what doesn't come from there: images hard-coded in the theme (often an SVG logo), page builder widgets, sliders and the images a plugin writes itself, such as a recipe card's photo. If you can't change the theme, WP Rocket can add the missing sizes (Media, Add missing image dimensions).
- Shopify's
image_tagfilter adds them. A theme or app that writes its own<img>doesn't: addwidth="{{ image.width }}" height="{{ image.height }}". - Next.js:
next/imageneeds width and height, or fill inside a parent that has its own size.
Ads and embeds: reserve the slot
Give the slot its height in CSS before the ad script runs, and make the iframe a block:
.ad-slot__inner { min-height: 50px; }
.ad-slot iframe { display: block; } An iframe is inline by default and sits on a line of text like a word, so with min-height alone the slot still grew a few pixels when the ad arrived: 0.007 was left. With display: block it was 0.
- Several sizes in one slot: reserve the size your ad server delivers most often (its history tells you), or the largest if blank space is acceptable. Use fluid sizes only below the first screen.
- AdSense Auto ads go where Google decides, so there is no slot to size. Exclude the first screen from them (Auto ads, Excluded areas) and put a manual ad unit there in a sized slot. If an ad management service places your ads, ask it to reserve the slot heights in the first screen.
- Empty slots: don't collapse a slot in the first screen when no ad comes back (
collapseEmptyDivsin Google Publisher Tag). The content then jumps up instead of down. Show a house ad or keep the space. - YouTube, maps and other embeds: width and height on an iframe don't reserve its height the way they do on an image. Put
aspect-ratio: 16 / 9; width: 100%; height: auto;on the iframe (or the ratio and the width on its wrapper), or show a sized image that becomes the iframe on click (YouTube, Google Maps).
Bars and banners inserted with JavaScript: overlay them or reserve their space
A newsletter bar or a consent banner that a script puts at the top of the page moves everything under it, and Lighthouse names nothing or the wrong element under that shift (see above). Make the bar or banner an overlay: with position: fixed at the bottom of the screen, in a corner or as a popup, it covers content and moves nothing. Consent tools such as OneTrust and CookieYes let you choose the banner's layout: pick a bottom bar or a box, and check in the Elements panel that the banner is fixed.
A bar that has to stay at the top needs its place reserved in the theme: an empty container with the bar's height, which the script fills.
<div id="acme-nl-slot"></div>
#acme-nl-slot { min-height: 48px; } That only works for a bar that always shows. Better still, put the bar in the HTML on the server.
To find the height, let the page load, select the bar with the element picker (Ctrl+Shift+C) and read its height in the Computed tab, at phone width (the device toolbar, Ctrl+Shift+M).
- WordPress plugins for newsletter, notice or promo bars: choose a bottom, floating or popup position in the plugin's settings. If it only does a bar at the top, reserve its height in your child theme's CSS or replace the plugin.
- Google Tag Manager: a Custom HTML tag that adds something visible almost always arrives after the first paint. Move that content into your template.
- Shopify apps (reviews, upsells, announcement bars): give the container of the app block a min-height (in the theme editor: select the section, then Custom CSS), or pick a popup layout if the app has one.
- Blocks rendered with JavaScript (reviews, related posts, a slider that sets its height in its script): render them on the server, or give the container its final height in CSS.
- Chat widgets belong in a fixed corner (the HubSpot chat shift).
Web font: make the fallback take the same space
When the web font is wider than the fallback, the headline breaks into more lines as it swaps in. Give the fallback the web font's size with a second @font-face, listed right after the web font:
@font-face {
font-family: "Young Serif Fallback";
src: local("Times New Roman"), local("TimesNewRomanPSMT"), local("Liberation Serif"), local("LiberationSerif");
size-adjust: 131%;
ascent-override: 79.85%;
descent-override: 27.94%;
line-gap-override: 0%;
}
.entry-title {
font-family: "Young Serif", "Young Serif Fallback", Georgia, serif;
} The ascent and descent overrides are the web font's own ascent and descent divided by size-adjust. To find size-adjust, block the font file (as above), look at the headline at phone width and raise size-adjust until the fallback breaks the lines at the same words as Young Serif. A width ratio isn't enough, because lines break at words: a calculator's 122% and the headline's own width ratio, 129.6%, still left it on two lines against three (CLS 0.029); at 131% the lines matched and the shift was gone.
font-display: optional also gave CLS 0, but the font arrived after the first paint, so first-time visitors read the whole page in the fallback. In Next.js, next/font sets the fallback for you. The values per platform and Safari's limits are on the Font display page.
Animations and late CSS
Animate transform. Every frame of an animation on top, left, height or margin is a layout shift: a title sliding in with left scored 0.047, the same slide with transform 0. Lighthouse doesn't name animations in this insight; it lists CSS animations like these under "Avoid non-composited animations" in Diagnostics. For a header that shrinks on scroll, keep the header's box at a fixed height and scale what is inside it. More in layout shift caused by CSS transitions and scroll-triggered animations.
A stylesheet loaded with the media="print" trick, or delayed by an optimisation plugin, moves the page when it arrives: one that made the header 16 px taller scored 0.027. Load that stylesheet the normal way, with a plain <link rel="stylesheet">. In an optimisation plugin, exclude the file from its option that delays CSS or loads it asynchronously.
Third-party content you can't change
For an ad's creative, or an app or plugin without a layout setting, the container around it is yours: give it the most likely height (how to find it). Or move it below the first screen: a slot that fills before the visitor scrolls to it moves only content that is off screen, and those shifts don't count. If neither works, replace the plugin or app.
How do you check that the layout shift fix worked?
Clear your caching plugin and your CDN first, or the test still gets the old page. Then run Lighthouse or PageSpeed Insights on mobile. You want CLS under 0.1 with the insight in grey, or CLS 0 with the insight under the passed insights, as on the recipe page above. Record again in the Performance panel, once with Record and reload and once while you scroll: the Layout shifts track should be empty.
CoreDash shows the new CLS per element from the next visits on. CrUX and Search Console show the last 28 days, so two weeks after the fix half of what they show is still the old site.
Not sure which of your shifts come from your theme and which from an ad or an app? In a Core Web Vitals power hour we record your page and open your field data together, and you leave with the slots and sizes to change.
Your Lighthouse score is not the full picture.
Your real users are on Android phones over 4G. I analyze what they actually experience.
Analyze field data