Shopify lazy loading images: what to defer and what to leave alone
How loading=lazy and Shopify's image_tag default work, which images to defer, and how to check that your theme has not lazy-loaded the wrong ones.
9 min read
Contents · 8 sections
- What does lazy loading actually do on a Shopify store?
- What does Shopify's `image_tag` do by default?
- Which images should never be lazy?
- Which images are worth deferring?
- Why do width and height matter more with lazy loading?
- What goes wrong with JavaScript lazy loaders?
- How do you check that your store does it right?
- What should you change first?
What does lazy loading actually do on a Shopify store?
Lazy loading is a single attribute on an img tag. According to web.dev, loading="lazy" defers loading of the image until it reaches a calculated distance from the viewport, and loading="eager" is the browser's default, which loads the image wherever it sits on the page. Without the attribute, Chrome still fetches images below the first screen, just at a lower priority. With it, those requests wait until the reader scrolls toward them.
That is the whole mechanism, and it explains both the benefit and the risk. A collection page with forty product cards has perhaps four on the first screen. Deferring the other thirty-six keeps their bytes off the network while the page is being judged, and the browser spends its bandwidth on what the reader can see. The risk is the reverse: the browser cannot lazy-load an image until it knows where the image sits on the page, so an image that is deferred but visible from the start loads later than it would have with no attribute at all. web.dev says it in one line: use loading="lazy" only for images outside the initial viewport.
The distance matters less than people fear. The same article says Chrome loads lazy images well before they are visible: since July 2020 the threshold on fast connections is 1,250 pixels from the viewport and on slower connections 2,500 pixels, and in Chrome's experiments on Android 97.5 percent of lazy images on 4G were fully loaded within 10 milliseconds of becoming visible. Those figures describe Chrome's behaviour as documented in 2020 and the browser can change them, so treat them as the reason the technique is safe, not as a constant you can build on.
What does Shopify's image_tag do by default?
The theme code that writes most images on a Shopify store is the image_tag filter, and its reference page states the default: unless you pass preload, loading is set to lazy automatically for images in sections further down the page, and you should not lazy-load images above the fold. The performance guidance is more precise: image_tag applies loading="eager" for sections one to three and loading="lazy" for sections four and later, and it applies eager whenever the section index is nil, which is the case in the theme editor, in static sections and in the Section Rendering API.
Two things follow for a store owner.
A theme that uses image_tag and sets no loading value is probably already doing the right thing. Do not add loading="lazy" to every image in the theme to "fix speed". The default already protects the first screen, and a blanket change removes that protection.
The default is a position rule, not a visibility rule. Section three of a mobile page can be well below the first screen, and section four can be above it on a short page. A rule based on position cannot know which images are visible, so the cases to check by hand are the ones where a large image sits at the boundary.
Which images should never be lazy?
One group: images that are visible when the page first renders. The first-screen hero, the first product gallery image, the logo if it is an image, and any other image that ends up in the first viewport on a phone. Shopify's page on the subject names the largest contentful paint image as the one to protect, and the hero image post covers how that image is requested, prioritised and sized. This post is about everything else on the page.
Note that loading="eager" does not make an image faster. The web.dev article is explicit: eager loads the image as usual, without delaying it further, and does not load it faster than an image with no attribute. For priority you want fetchpriority="high", once per page, on the image that decides the load time. An image with both loading="lazy" and fetchpriority="high" is still delayed while off-screen, so the combination is not useful.
Which images are worth deferring?
Anything the reader has to scroll to reach. On a Shopify store that usually means:
- Product cards in the lower rows of a collection or search results page.
- Gallery images after the first on a product page, and thumbnails that are not in view.
- Images in the lower sections of the home page: testimonials, Instagram feeds, brand logos, footer promotions.
- Images inside blog articles after the first screen.
- Product recommendation blocks at the bottom of the product page.
The gain is a lighter first load, which feeds the page-weight problem described in Shopify page weight. It is not a cure for a heavy image: a 2 MB upload that is deferred is still a 2 MB image when the reader scrolls to it, and the right fix there is a smaller file.
Three edge cases from web.dev's documentation are worth knowing:
- Hidden images. Chrome, Safari and Firefox do not load lazy images that are
display: none, on the image or a parent. Other hiding techniques, such asopacity: 0, still make the browser load the image. A slideshow built onopacitytransitions therefore downloads every slide unless the slides are managed another way. - Carousels. Since Chrome 121, horizontally scrolling images use the same thresholds as vertical scrolling, so carousel images load before they are visible, at the cost of more downloads.
pictureelements. You only needloadingon the fallbackimg, not on thesourceelements.
CSS background images cannot be lazy-loaded with this attribute, because it only works on img tags. If a section uses background-image for content that matters, the cleaner fix is an img tag positioned to fill the section.
Why do width and height matter more with lazy loading?
Because a lazy image is, by definition, one the browser does not have yet, so the browser has to reserve space for it. image_tag writes width and height from the image's own dimensions, per its reference, and the browser uses that ratio to hold the space before the file arrives. web.dev's lazy-loading article says the dimensions are good practice for any img, but that lazy loading makes them more important: if an image does not load at the right time and has no dimensions, it adds to Cumulative Layout Shift.
There is a second, less obvious failure. Without width and height, the article explains, image dimensions default to 0×0 pixels, so on a gallery the browser may decide every image fits inside the viewport at the start and load all of them, which defeats the lazy loading. If a collection page loads every product image on arrival despite the attribute, missing dimensions are the first thing to check. The wider layout shift picture is in Shopify cumulative layout shift.
Themes break this when someone passes width: nil to image_tag, which the filter's reference says prevents the attribute from being added, or when custom code builds the img tag by hand.
What goes wrong with JavaScript lazy loaders?
Older themes and some apps use a library such as lazysizes, which replaces src with data-src and swaps it back when the image approaches the viewport. Shopify's performance guidance flags this as an anti-pattern for critical images: it hides the image URL from the browser's preload scanner until JavaScript runs. For images below the fold it also adds a script to the page for something the browser now does natively, and web.dev notes that browser-level lazy loading needs no custom code or separate library.
Google adds a search angle. Its documentation on lazy-loaded content says Google Search does not interact with your page, so a lazy-loading method must load content whenever it is visible in the viewport, without relying on scrolling or clicking. Built-in lazy loading for images and iframes, the IntersectionObserver API and libraries that use it all qualify. Google also says not to lazy-load content likely to be visible immediately on opening the page, which is the same rule as above, arrived at from the crawling side.
If your theme uses data-src, the migration Shopify describes is short: put the real URL back in src, add loading="lazy" to images outside the first screen, and remove the library's script tag.
How do you check that your store does it right?
Three checks, none needing a tool beyond a browser.
- View the rendered HTML of a collection page and look at the first row of product images and the tenth. The first row should have no
loading="lazy"; the tenth should. Everyimgshould havewidthandheight. - Throttle the network in DevTools and scroll a long collection page with the Network panel open. Images should start downloading as you approach them, not all at once on arrival.
- Inspect a live URL in Search Console. Google's documentation says that if your image or video URLs appear in the
srcattribute of theimgorvideoelements in the rendered HTML, the setup works. If they appear only in adata-srcattribute, search engines may not be seeing the images.
Also check the opposite problem in the page's markup: if the hero or the first product image carries loading="lazy", fix that before anything else on this page. The store audit checklist puts it in the same section as image weight.
What should you change first?
- Leave
image_tagdefaults alone unless one of the checks above fails. - Remove
loading="lazy"from any image that is on the first screen on a phone, and move it to asection.index > 3test rather than<= 3if the theme overrides the default, since a comparison againstnilis false in Liquid. - Restore
widthandheighton any image missing them. - Replace script-based lazy loaders with the attribute on the images that need it.
- Re-run the lab test, then watch real-user LCP and layout shift over the following weeks, because the lab only simulates one device.
If you would rather have all of this measured across the whole store, ranked against everything else that is slowing it down and written up, that is the Store Teardown.