On a lot of Shopify stores I audit, the biggest image on the homepage is also the last one the browser bothers to fetch.
It's the hero banner. It's almost always the Largest Contentful Paint element on mobile. And somewhere in a custom section, someone has written loading: 'lazy' on it, so the browser waits until layout is done before it even starts the download.
Here's how to check for it and fix it in Liquid, without an app.
How it happens
Shopify's image_tag filter already does the sensible thing. Per the docs, loading is set to lazy automatically for images in sections further down the page, and images near the top are left alone. If you just write:
{{ section.settings.image | image_url: width: 2400 | image_tag }}
you get a good default.
The trouble starts when a theme or a freelancer "optimises" every image by hand:
{{ section.settings.image | image_url: width: 2400 | image_tag: loading: 'lazy' }}
Passing loading yourself overrides the default. Now the hero is lazy too, wherever the merchant puts that section, including first on the page. I've also seen the same thing come from old themes that used a lazy-loading JavaScript library with data-src attributes, which is even worse, because the real URL isn't in the HTML at all.
How to check in two minutes
- Run the homepage through PageSpeed Insights on mobile.
- Open the "LCP request discovery" insight. If it says lazy load is applied to the LCP image, or that
fetchpriority=highshould be applied, you've found it. - Confirm in the HTML: view source, search for the hero's file name, and look at its
<img>tag forloading="lazy".
Do the check on the live storefront, not inside the theme editor. That matters for the fix below.
The fix: decide by section position
Every section has section.index, its 1-based position within its location (the template, or a section group like the header). That's exactly what you need: the first section on the page gets priority, everything below is lazy.
Dawn does this in sections/image-banner.liquid. Simplified (Dawn works out sizes and widths from the section settings; I've left those out):
{%- liquid
assign fetch_priority = 'auto'
if section.index == 1
assign fetch_priority = 'high'
endif
-%}
{{
section.settings.image
| image_url: width: 3840
| image_tag:
sizes: '100vw',
fetchpriority: fetch_priority
}}
Two things to notice. Dawn doesn't pass loading at all, it lets image_tag decide. And it uses fetchpriority, which isn't a named option of the filter; image_tag passes through any HTML attribute you give it as a parameter, so fetchpriority: 'high' ends up on the <img>.
If your section has to set loading explicitly (say, a custom slideshow where only the first slide should load early), make it conditional instead of hardcoded:
{%- liquid
if section.index == 1 and forloop.first
assign img_loading = 'eager'
assign img_priority = 'high'
else
assign img_loading = 'lazy'
assign img_priority = 'auto'
endif
-%}
{{ block.settings.image | image_url: width: 2400 | image_tag: loading: img_loading, fetchpriority: img_priority, sizes: '100vw' }}
Only the first slide of the first section gets priority. Slides two and three can wait.
The catch: section.index is sometimes nil
The docs list three cases where section.index returns nil:
- static sections (rendered with
{% section 'name' %}) - while rendering in the online store editor
- when the section comes back through the Section Rendering API
With the code above, nil == 1 is false, so you fall back to lazy and auto. That's the safe direction, but it explains two things that confuse people. First, the editor preview will never show fetchpriority="high", so test on the live store. Second, if your hero is a static section in theme.liquid, the check never passes, and you need to set it explicitly for that one section.
What about preload: true?
image_tag also takes preload: true, which makes Shopify send a preload resource hint as a Link HTTP header. It doesn't change the <img> tag itself, and Shopify's own docs say to use it sparingly.
In practice, fetchpriority: 'high' on an <img> that's already in the HTML is usually enough. I'd only add preload when the LCP image is a CSS background or sits deep in the markup where the browser finds it late. Preloading two or three images just means they fight each other for bandwidth.
Don't forget sizes
One more thing I fix in the same pass. Without a sizes attribute the browser assumes the image is full viewport width, which is right for a full-bleed banner but wrong for a half-width image next to text. On a phone, that can mean downloading a much bigger file than the slot needs. Set sizes to match the layout, like Dawn does with (min-width: 750px) 50vw, 100vw for split banners.
Quick checklist
- Search your theme for
loading: 'lazy'andloading="lazy". Anything in a section that can sit at the top of the page needs a condition, not a hardcoded value. - Give the first image of the first section
fetchpriority: 'high'. - Leave everything else to
image_tag's default. - Test on the live storefront, then rerun PageSpeed Insights and check the LCP request discovery insight again.
It's a ten-minute change that rarely needs an app. If you'd rather someone else went through a custom theme for this kind of thing, it's part of what we do in our Shopify theme development work.
Top comments (0)