Lazy Loading Product Images Without Hurting LCP

Everyone knows not to lazy-load the hero. The harder calls are the collection grid, the gallery carousel, the JavaScript loader your theme shipped with, and the placeholder you show while images arrive.

|image optimization e-commerce SEO conversion optimization

Lazy loading product images is the cheapest performance win on an e-commerce site and one of the easiest ways to make Largest Contentful Paint worse. One attribute, loading="lazy", tells the browser to wait until an image is near the viewport before requesting it. On the right image it saves megabytes. On the wrong image it pushes your most important asset to the back of the queue.

The basic rule is well known by now: never lazy-load the LCP image, which on a product page is almost always the first gallery photo. We cover that fix in depth in our guide to product image LCP. This article is about everything that rule does not settle: where the fold actually sits on each page template, how many tiles in a collection grid should load eagerly, what to do with carousels and zoom images, why script-based lazy loaders are riskier than the native attribute, and how to confirm the result in field data rather than a single lab run.

The goal is a per-template loading policy you can hand to whoever maintains your theme, one that decides image by image rather than with a single global switch.

What the browser actually does when you lazy-load product images

Native lazy loading is not "load when the image scrolls into view". Browsers start fetching a lazy image when it comes within a distance threshold of the viewport, so it is usually ready by the time the shopper reaches it. Chromium's documented thresholds are generous: roughly 1,250px on fast connections and 2,500px on slower ones. Those numbers matter for product pages, because 1,250px on a phone is well over a full screen of content.

1,250pxChromium load-ahead distance on fast connections
2,500pxLoad-ahead distance on slow connections
2.5s"Good" LCP at the 75th percentile of visits

Two consequences follow. First, lazy loading saves less than people expect on short pages: if your whole product page is three screens tall, most of the "lazy" images load on arrival anyway. The real savings come on long collection pages, search results and homepages with dozens of tiles. Second, the cost of a lazy attribute on an above-the-fold image is not the download itself. It is the delay. The browser cannot decide whether a lazy image is near the viewport until it has done layout, which needs CSS, so the request starts late and outside the preload scanner's early pass.

That is why the right question is never "should we lazy load?" It is "which images, on which template, at which viewport, are candidates for the first screen?" Anything that might be the first thing a shopper sees gets eager loading. Everything else is lazy.

Where the fold sits, template by template

There is no single fold. It moves with the template, the device and whatever sits above the product imagery: announcement bars, sticky headers, promo banners and cookie notices all push images down on some pages and not others. Work it out per template, at the smallest common phone viewport and at a common laptop size, because the LCP element on mobile is often a different image from the one on desktop.

TemplateLikely LCP imageLoad eagerlyLazy-load
Product pageFirst gallery imageImage 1, plus fetchpriority="high"Gallery images 2+, reviews photos, related products
Collection pageFirst tile, or a collection banner if you have oneFirst visible row(s) on mobile and desktopEvery tile below those rows
Search resultsFirst result tileSame rule as collectionsRemaining results, "load more" batches
HomepageHero banner or lifestyle imageHero onlyFeatured collections, lookbook, blog cards
Mega menu / drawersNever LCPNothingAll menu imagery (it is hidden on load)
Cart and upsell blocksUsually textLine-item thumbnails if above the foldCross-sell carousels

The homepage and collection page are where the defaults most often go wrong. A homepage whose hero is a slideshow can have a different LCP element depending on which slide renders first, and a collection page with a text-only header on desktop may promote the first product tile to LCP on mobile. Check both viewports before you set the policy.

Watch for mobile-only banners

A promo strip or app-download banner that only appears on phones can push the first gallery image below the fold on small screens, or push it back up when the banner is dismissed. Base the policy on what a first-time visitor sees, because that is who the field data measures.

Collection grids: decide by position, not by component

Collection and search grids are where lazy loading earns its keep, and also where a blanket setting does the most damage. A product-card component does not know where it is on the page. If the card template hard-codes loading="lazy", the first row of every collection is lazy. If it hard-codes eager, a 48-product collection requests 48 images on arrival.

The fix is to pass the card its position and decide there. A simple rule is to load eagerly the number of tiles that fit in the first screen at your widest common layout, and lazy-load everything after that:

{% for product in collection.products %}
  {% if forloop.index <= 4 %}
    {% assign loading = 'eager' %}
  {% else %}
    {% assign loading = 'lazy' %}
  {% endif %}
  {% render 'product-card', product: product, loading: loading %}
{% endfor %}

Four covers a two-column mobile grid (two rows visible) and a four-column desktop grid (one row visible). If your desktop grid is five or six columns wide, raise the number to match. Resist setting it much higher: every eager image competes for bandwidth with the one that actually becomes LCP. Give fetchpriority="high" to the first tile only, not to all of them. Priority only works when it is scarce.

Illustrative: share of a 48-tile collection requested on arrival
All eager
48 of 48
All lazy
~8 (late)
First 4 eager
~8 (on time)

The chart makes the point that "all lazy" and "first four eager" end up requesting a similar number of images on arrival, because the load-ahead distance pulls in the next few rows either way. The difference is timing. In the all-lazy version the first row waits for layout. In the position-aware version it is requested straight from the HTML.

Many modern Shopify themes already make this call by section position rather than per card, lazy-loading images only in sections further down the template. That works for banners and one-image sections. It does not help a single grid section that holds both the first row and the fortieth tile, so check how your theme handles the grid itself.

Galleries, carousels and zoom images

Product galleries hide most of their images. Slides two to eight sit off-canvas in a carousel, and the full-resolution zoom image only matters if the shopper asks for it. Each needs a different treatment.

  • Slide 1: eager, fetchpriority="high", rendered in the server HTML. If your gallery is built entirely in JavaScript, the first image does not exist until the script runs. Server-render it, or at minimum add a preload for it.
  • Slides 2 and beyond: loading="lazy". Modern Chromium applies the same load-ahead logic to images sitting off-screen in a horizontal scroll container, so the next slide is usually fetched before the swipe finishes. If your carousel hides slides with display: none instead of positioning them off-canvas, lazy images inside it will not load until they are shown. That can make a swipe land on a blank frame, so test on a throttled connection.
  • Thumbnail strip: small and cheap. Eager is fine if the strip is visible on load, but keep the files genuinely small. A thumbnail rail that downloads full-size originals scaled down in CSS is one of the most common sources of wasted bytes on product pages.
  • Zoom and lightbox images: load on intent, not on page load. Request the high-resolution version when the shopper hovers, taps or opens the lightbox. Our product image zoom guide covers the resolution side of that trade-off.

Variant swatches are the edge case. When a shopper picks a colour, the gallery swaps to that variant's images, and a shopper who changes colour on arrival will wait if those images were lazy. If most traffic lands on a default variant and some shoppers switch straight away, prefetch the first image of each variant after the page has loaded. That is cheap, and it removes the blank moment after a swatch click.

Native attribute or JavaScript lazy loader?

Before browsers supported loading="lazy", themes lazy-loaded with JavaScript: the real URL sat in a data-src attribute and a script swapped it into src as the image approached the viewport. Plenty of themes and apps still ship this pattern, sometimes alongside the native attribute. It has costs that the native version does not.

Script-based (data-src)

  • No image request until the script downloads, parses and runs
  • The preload scanner cannot see the URL, so eager treatment is impossible
  • Images that depend on scroll events may never render for crawlers
  • A script error leaves every product tile blank
  • Often combined with a placeholder that shifts layout when swapped

Native loading="lazy"

  • The browser schedules requests with no script dependency
  • Eager images are discovered straight from the HTML
  • Supported by Google's rendering for indexing
  • Degrades safely: at worst an image loads early
  • Works with srcset and sizes without extra code

The search side is worth spelling out. Google's guidance is that content should not depend on user actions such as scrolling or clicking to load. Its renderer does not scroll the way a shopper does. Native lazy loading and viewport-based loaders using IntersectionObserver are generally fine. Loaders that wait for a scroll event are not, and product images that never load for the renderer cannot rank in image search. If you want your product photos to appear in Google Images and Shopping surfaces, the URL should be in a real src or srcset in the served HTML. Our product image SEO guide covers the alt text and file name half of that.

If your theme uses a script loader, the usual migration is to move the URL back into src, add loading="lazy" to below-fold images, and delete the library. Check apps as well as the theme: review widgets, recently-viewed carousels and upsell blocks often bring their own loader.

Placeholders, layout shift and consistent framing

Lazy loading makes the space an image will occupy visible before the image itself, which means Cumulative Layout Shift and lazy loading are the same problem seen from two angles. Every lazy image needs its box reserved before it loads: explicit width and height attributes, or a CSS aspect-ratio on the container. Without that, each tile that arrives pushes the grid down and the shopper's thumb lands on the wrong product.

What fills the box while the image loads is a design decision with performance consequences:

PlaceholderCostVerdict
Solid colour matching your product backgroundZero bytes, one CSS ruleBest default for catalog grids
Blurred low-quality preview (LQIP)Small, but extra markup or requestsNice on editorial and lifestyle imagery
Spinner or animated shimmerScript or animation cost, visual noiseAvoid on dense grids
Nothing (transparent box)Zero, but white flashes on coloured pagesAcceptable on white-background stores

Two details are worth knowing. Chrome ignores very low-detail images as LCP candidates, so a blurred placeholder does not "count" as your LCP. The metric waits for the real image, and a placeholder cannot fake a good score. The upside is that it cannot hurt your score either. Second, a solid-colour placeholder only looks clean if your product images actually share that background. If half the catalog sits on #FFFFFF and half on a warm off-white, any single placeholder colour flashes on one half.

Consistency pays off here as well as for merchandising. When every image in a grid has the same aspect ratio, the same plate colour and a consistent product scale, you can reserve every box with one CSS rule and fill it with one colour, and the grid looks finished before a single image has loaded. That is part of why Retouchable's product outputs end with a deterministic framing step that paints an exact plate colour and scales each cutout to a fixed rule: the consistency that makes a grid look professional also makes it easy to load gracefully.

Verify it in field data, not a single lab run

A Lighthouse run on your laptop tells you whether the policy is wired up correctly. It does not tell you whether shoppers got faster pages. Use the lab to check the mechanics and field data to judge the outcome.

  1. Check the HTML, not the DOM. View the page source (not the inspector, which shows the page after scripts have run) and confirm the first gallery image and the first grid row have a real src and no loading="lazy".
  2. Find the LCP element at both viewports. The Performance panel in Chrome DevTools names the LCP element. Run it at a phone size and a laptop size, on every template in the table above.
  3. Read the Lighthouse flag. Lighthouse explicitly warns when the LCP image was lazily loaded or discovered late. Treat that warning as a bug, not a suggestion.
  4. Throttle and swipe. On a throttled mobile profile, swipe the gallery and scroll a long collection. Blank frames mean the policy is too aggressive, usually because of hidden slides or a script loader.
  5. Segment field data by template. Search Console's Core Web Vitals report groups similar URLs, and real-user monitoring can split LCP by page type. A sitewide average hides a collection template that regressed while product pages improved.
  6. Wait for the 28-day window. Chrome UX Report data is a rolling 28-day window at the 75th percentile, so a change takes about four weeks to show fully. Judge it then, not the next morning.
The one-line policy

Eager for anything that could be the first screen on any common viewport, fetchpriority="high" on the single most likely LCP image, native lazy loading for everything else, a reserved box and a flat background colour on every image. Most stores that follow that sentence never need a lazy-loading library again.

Lazy loading is one lever among several. File weight, format and correctly sized variants decide how long the eager images take once they are requested. Our guides to srcset and sizes and image CDN setup cover those next steps.

Frequently Asked Questions

Should I lazy load all product images?

No. Lazy-load every image except those that could appear in the first screen: the first product gallery image, the first row of a collection or search grid, and the homepage hero. Lazy-loading those delays the request until layout is complete, which directly worsens Largest Contentful Paint.

Does lazy loading hurt SEO for product images?

Native loading="lazy" does not: Google's renderer supports it and the image URL is still in the HTML. JavaScript loaders that keep the URL in a data-src attribute and wait for scroll events can stop images from rendering for crawlers, which keeps them out of image search.

How many products in a collection grid should load eagerly?

As many as fit in the first screen at your widest common layout. For a two-column mobile grid and a four-column desktop grid that is usually the first four tiles. Give fetchpriority="high" to the first tile only.

Should carousel images be lazy loaded?

The first slide should be eager and server-rendered. Slides two onward can use native lazy loading, as long as the carousel positions hidden slides off-canvas rather than using display: none, which stops lazy images loading until they are shown.

Does a blurred placeholder improve my LCP score?

No. Chrome ignores very low-detail images as LCP candidates, so the metric waits for the real image. Placeholders help perceived speed and, with reserved dimensions, prevent layout shift, but they do not change LCP.

Make your catalog grid load as cleanly as it looks

Retouchable frames every product on the same plate colour and scale, so one reserved box and one placeholder colour work across your whole catalog.

Try Retouchable Free No credit card required