Stopping Layout Shift from Product Images

How Cumulative Layout Shift is actually scored, how to trace a shift back to the image that caused it, and the fixes for the places width and height attributes do not reach.

|e-commerce SEO image optimization conversion optimization

A single product image that loads without a reserved box can push the rest of the viewport down far enough to score 0.5 on Cumulative Layout Shift. That is five times Google's "good" limit, from one element. Layout shift from product images is rarely about the hero alone, though. Most stores that still fail CLS have already added width and height attributes and are losing points somewhere less obvious: a gallery that changes height when a shopper picks a colour, a carousel that renders every slide stacked before its script runs, or a "Sale" badge that an app injects above the image half a second late.

This guide skips the basics that our LCP guide already covers. It explains how the score is calculated, so you can predict which shifts matter. Then it covers how to find the exact element that moved and how to fix the product image patterns that dimension attributes alone do not solve.

How CLS is scored, and why product images dominate it

Every unexpected movement of a visible element produces a layout shift score, the product of two fractions:

  • Impact fraction. The share of the viewport covered by the moving element's before and after positions combined.
  • Distance fraction. How far it moved, as a share of the viewport's largest dimension.

Worked example: on an 800px-tall phone viewport, a 400px product image arrives without a reserved box and pushes the title, price and buy button (400px of content) down by 400px. The moved block spanned 0–400px before and 400–800px after, so the impact fraction is 800/800 = 1.0. It moved 400px of 800, so the distance fraction is 0.5. The score for that one shift is 1.0 × 0.5 = 0.5.

Shifts are grouped into session windows. A window closes after a one-second gap with no shifts, or after five seconds at most, and your page's CLS is the worst window rather than the sum of everything. Google assesses it at the 75th percentile of real visits.

≤ 0.1"Good" CLS
> 0.25"Poor" CLS
500 msGrace period after a tap or keypress
5 sMaximum session window

The formula explains why product imagery dominates. Images are the largest elements on a PDP or collection page, so they produce the biggest impact fractions. They also load last, so they move things after the shopper has started reading. A late web font nudges a line of text by a few pixels. A late image moves half the screen.

Finding the image that actually moved

Lighthouse and a single PageSpeed Insights lab run only measure shifts during initial load, with no scrolling and no taps. Field CLS covers the whole visit, including scrolling a collection grid, opening a gallery and switching variants. That is why a store can show 0 in the lab and fail in the Chrome UX Report. If the numbers disagree, trust the field data and reproduce the visit by hand.

Three ways to attribute a shift to an element, from quickest to most thorough:

  1. Chrome DevTools, Performance panel. Record while you load, scroll and tap through a product page. Layout shifts appear as their own track, and selecting one highlights the nodes that moved.
  2. A console observer. Paste this into the console, then use the page as a shopper would. It logs every shift that counts, with the elements involved:
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue; // excluded from CLS
    console.log(entry.value.toFixed(3),
      entry.sources.map(s => s.node));
  }
}).observe({ type: 'layout-shift', buffered: true });
  1. Real-user attribution. The attribution build of Google's web-vitals library reports the largest shift target for each visit. Send it to your analytics and group by selector to see which template and element cause most of your field CLS.
Read the sources carefully

The node reported is the element that moved, not the one that caused the move. If the logged node is your price block, look directly above it for the image, badge or carousel that changed size.

Where product images cause layout shift

After dimensions are declared on the main image, the remaining shifts almost always come from one of these patterns:

SourceWhat the shopper seesCounts toward CLS?
Gallery images with mixed aspect ratiosBuy button jumps when they swipe to a portrait shotYes, if the new image lands > 500 ms after the swipe
Variant swatch swaps the main imagePage height changes when a colour is pickedOften: slow image loads fall outside the input grace period
Carousel initialised by JavaScriptAll slides stacked, then collapse into oneYes, and usually the largest single shift on the page
Badges or review stars injected by appsImage slides down as a label appears above itYes
<picture> with different ratios per breakpointWrong box reserved on mobileYes, if sources lack their own dimensions
Lazy-loaded grid tiles with no reserved boxGrid lurches while scrollingYes
"Load more" appending below the viewportNothing, since new tiles arrive off-screenNo

The variant swap deserves attention because it catches careful teams. Shifts within 500 milliseconds of a tap are excluded because the shopper caused them. But if the new colour's image takes 800 ms to download and is a different shape from the old one, the resize lands outside that grace period and counts in full. On a slow mobile connection, every colour change becomes a scored shift.

Fixing galleries and variant swaps

The principle: the box belongs to the gallery, not to the image. Give the gallery container a fixed aspect ratio and let each image fit inside it. Changing which image is inside then cannot change the page's geometry.

.pdp-gallery__frame {
  aspect-ratio: 4 / 5;      /* your catalog's standard */
  background: #f6f6f4;      /* match your plate colour */
}
.pdp-gallery__frame img {
  width: 100%;
  height: 100%;
  object-fit: contain;      /* never crop the product */
}

Use object-fit: contain, not cover. Cover guarantees a filled box by cropping, which cuts off shoe toes and bag handles on any image that doesn't match the frame. Contain keeps the whole product visible and letterboxes the difference. If the letterboxing is visible, that is a sign the source images are inconsistent, not a CSS problem (more on that below).

For variant swaps, keep the old image on screen until the new one is ready to paint:

async function showVariant(img, url) {
  const next = new Image();
  next.src = url;
  await next.decode();      // downloaded and decoded
  img.src = url;            // swap in one frame
}

Start the download earlier by preloading the image when a shopper hovers over or touches a swatch. That usually gets the swap inside the 500 ms grace period. Even when it doesn't, a fixed-ratio frame means there is nothing to shift.

Carousels, badges and the grid

Carousels. Most JavaScript sliders render their slides as a plain vertical list until the script runs, and then collapse them into one frame. On a PDP, that collapse is often the largest shift on the page. Style the pre-initialised state so it already looks like the finished carousel: a fixed-ratio frame showing only the first slide, with the rest hidden or laid out horizontally with overflow: hidden. A native CSS scroll-snap carousel avoids the problem entirely because it needs no initialisation to reach its final layout.

Badges and overlays. "Sale", "New", "Low stock" and review-star widgets are frequently injected by apps after the page renders. Anything that sits in normal document flow above or beside an image will push it. Position badges absolutely inside the image frame so they overlay without taking space. Reserve a fixed-height slot for review stars even before the widget loads.

Collection grids. Put the aspect ratio on the tile, not only on the <img>, so the tile keeps its size even when an image fails to load or a lazy loader swaps in a placeholder. With <picture>, add width and height to each <source> when breakpoints use different ratios. Current major browsers honour them, and without them the browser reserves the fallback image's shape at every size. The lazy-loading side of this, including which tiles to load eagerly, is covered in lazy loading product images without hurting LCP.

Shift-prone

  • Image sets its own height from its file
  • Swatch click sets src immediately
  • Slider stacks slides until JS runs
  • Badges in document flow above the image

Stable

  • Frame owns a fixed aspect ratio
  • Swap after decode(), preloaded on hover
  • Pre-init CSS matches the final layout
  • Badges absolutely positioned in the frame

Mixed aspect ratios are a content problem

Every fix above is easier when the images themselves agree. A catalog where some products were shot square, some portrait and some cropped by whoever uploaded them forces a trade-off on every template. You can fit them all in one frame and accept visible letterboxing, crop them to fit and lose parts of the product, or let each image set its own height and accept the shift.

Standardising the source images removes that trade-off. If every product image is exported on the same canvas, at the same ratio, on the same plate colour, one CSS rule reserves every box on every template. The placeholder colour matches every image, and a variant swap can't change the page's geometry because every variant has the same shape. Retouchable's product outputs finish with a deterministic framing step for this reason: it measures the cutout, scales it by a per-category rule and places it on a fixed canvas with an exact plate colour. The result is layout stability that doesn't depend on every template being coded defensively.

If re-exporting the catalog isn't practical yet, audit it first. Group your product images by aspect ratio, then fix the templates for the ratios that account for most images. Treat the outliers as a content backlog rather than a CSS problem.

A verification checklist before you ship

CLS fixes are easy to break with the next theme update or app install, so treat them like tests:

  1. Throttle and click. In DevTools, set network throttling to a slow mobile profile. Then load a PDP, swipe the gallery, change variants twice and scroll to the recommendations with the observer snippet running. Any logged value above about 0.01 deserves a look.
  2. Test the image that fails. Block one product image URL in DevTools and reload. The frame should keep its size with the placeholder colour, not collapse to zero height.
  3. Check each breakpoint. Art-directed <picture> elements and responsive grids reserve different boxes at different widths. Test phone, tablet and desktop widths separately. The srcset and sizes guide covers how those variants are chosen.
  4. Re-test after every app install. Review, badge and upsell apps are the most common way a stable page starts shifting again.
  5. Confirm in the field. Lab tests show what can shift. The Chrome UX Report and Search Console's Core Web Vitals report show what real shoppers experienced, with a lag of about four weeks for the 28-day window to roll over.
Worked example: one unreserved 400px image on an 800px viewport
"Good" limit
0.10
"Poor" threshold
0.25
One unreserved image
0.50

Layout shift is one of the few Core Web Vitals problems you can remove completely rather than just reduce. It comes down to geometry: when every image lives in a box whose size is known before any bytes arrive, product images have nothing left to move.

Frequently Asked Questions

Do width and height attributes still matter if my CSS sets height: auto?

Yes. Modern browsers use the width and height attributes to calculate an aspect ratio and reserve the box, even when CSS sets width: 100% and height: auto. The attributes supply the ratio and your CSS supplies the size. Removing them is one of the most common causes of image layout shift.

Does changing the product image when a shopper picks a colour count toward CLS?

Only shifts within 500 milliseconds of the tap are excluded. If the new variant image takes longer to load and has a different shape, the resulting shift is counted. A fixed aspect-ratio gallery frame, plus waiting for the new image to decode before swapping it in, prevents this.

Why is my Lighthouse CLS 0 when Search Console says CLS is poor?

Lighthouse measures only the initial page load, with no scrolling or interaction. Field data from real Chrome users covers the whole visit: scrolling grids, swiping galleries, switching variants and late-loading app widgets. Reproduce a real visit with the DevTools Performance panel or a layout-shift observer to find the gap.

Should I use object-fit: cover or contain for product images?

Use contain for product imagery so the full product is always visible inside a fixed frame. Cover fills the box by cropping, which can cut off parts of the product when an image does not match the frame ratio. Cover is fine for decorative or lifestyle banners where cropping is acceptable.

What is a good CLS score for an e-commerce store?

Google classes 0.1 or less as good and anything above 0.25 as poor, measured at the 75th percentile of real page visits. Because a single unreserved product image can score 0.5 on its own, product pages that reserve space for all imagery usually land well under 0.1.

Make every product image the same shape

Retouchable frames every product on a consistent canvas and plate colour, so one layout rule holds for your whole catalog.

Try Retouchable Free No credit card required