TL;DR: On most Shopify stores the Largest Contentful Paint element is the hero or first product image, so Shopify LCP image optimization is the fastest way to move your Core Web Vitals score. The five fixes that matter: never lazy-load the LCP image, mark it with fetchpriority="high", give it an explicit sizes attribute so the browser picks the right size immediately, right-size the image through image_url, and stop third-party scripts from competing for bandwidth. Most stores can cut LCP by 20 to 40 percent in one afternoon.
In this guide
- Why the LCP image decides your Shopify speed score
- How to find your LCP element on any Shopify page
- Fix 1: Never lazy-load the LCP image
- Fix 2: Use fetchpriority="high" on exactly one image
- Fix 3: Explicit sizes and widths, no JavaScript guessing
- Fix 4: Right-size through image_url and let the Shopify CDN work
- Fix 5: Clear the path, so the image is not fighting your apps
- FAQ
Why the LCP image decides your Shopify speed score
Largest Contentful Paint measures how long it takes for the biggest visible element in the viewport to render. On a Shopify homepage that element is almost always the hero banner. On a collection page it is one of the first product cards. On a product page it is the featured media. In other words, Shopify LCP image optimization is not one tactic among many; it is the tactic, because the image is the LCP element on the overwhelming majority of storefront pages.
Google's threshold for a good LCP is 2.5 seconds at the 75th percentile of real visits. Shopify stores frequently land between 3 and 5 seconds on mobile, and when you open the trace, the image is the thing waiting. It waited for JavaScript to run before the browser even knew the URL. It downloaded at low priority behind a dozen app scripts. It arrived at 2,400 pixels wide for a 390 pixel phone screen. None of these are hard problems, but they compound, and they are hidden inside theme code that most merchants never open.
The revenue case is well established. Our own analysis of Shopify speed and conversion data shows that every 100 milliseconds of load time shifts conversion rate measurably, and LCP is the metric that most closely tracks what a shopper experiences as "the page is here." If you only have time to fix one thing before peak season, fix the LCP image.
How to find your LCP element on any Shopify page
Before changing any Liquid, confirm which element the browser treats as LCP. Guessing is risky because the answer changes by device: a two-column hero may be LCP on desktop while the headline text wins on mobile because the image collapses below the fold.
Open Chrome DevTools, go to the Performance panel, and record a page load with mobile throttling enabled. The Insights tab will highlight the LCP element and show the phases that make up its timing: time to first byte, resource load delay, resource load duration, and element render delay. Those four phases tell you exactly which of the fixes below will help. A long resource load delay means the browser found the image late (lazy loading or JavaScript injection). A long load duration means the file is too large or deprioritized. A long render delay usually means a font or CSS is blocking paint.
Run the same check on your homepage, best-selling collection, and best-selling product. Then compare the lab trace against your field data in Search Console or CrUX, because the two often disagree. We cover why in lab data vs field data for site speed. Field data is what Google ranks on; lab data is what you debug with.
Fix 1: Never lazy-load the LCP image
This is the single most common and most damaging mistake in Shopify themes, and Shopify's own performance documentation now calls it out explicitly. When Liquid renders an img tag with a real src attribute into the HTML, the browser's preload scanner discovers the URL while it is still parsing the document, before any CSS or JavaScript has run. That head start is worth hundreds of milliseconds. Lazy loading throws it away.
There are two versions of the anti-pattern. The first is loading="lazy" on the hero image itself, which deprioritizes it and defers the download until an IntersectionObserver fires. The second, older and worse, is a JavaScript lazy loader such as lazysizes that replaces src with data-src. The preload scanner cannot see data-src at all, so the browser does not know the image exists until the library has downloaded, parsed, and executed.
The fix is to set the hero to loading="eager" (or simply omit the attribute, since eager is the browser default). Shopify's image_tag filter helps here: it automatically applies eager loading for the first three sections on a page and lazy for sections four and later, based on section.index. If your theme hardcodes loading: 'lazy' in the hero section, remove it. If you want to be explicit, this is the pattern Shopify recommends:
{%- liquid assign hero_loading = 'eager' if section.index > 3 assign hero_loading = 'lazy' endif -%} {{ section.settings.image | image_url: width: 1600 | image_tag: loading: hero_loading, widths: '600, 900, 1200, 1600', sizes: '100vw' }}
Note the positive comparison, section.index > 3, rather than section.index <= 3. In the theme editor, in static sections, and in the Section Rendering API, section.index is nil, and nil comparisons are falsey in Liquid. The positive form keeps nil cases eager, which is what you want.
For collection grids, the first section may contain 50 product cards. Use forloop.index to eager-load only the first four and lazy-load the rest. If your theme still ships a lazy loading library, migrate off it: replace data-src with src, add loading="lazy" to below-the-fold images, and remove the script. Modern browsers handle native lazy loading well, and you drop a render-blocking dependency in the process.
Fix 2: Use fetchpriority="high" on exactly one image
Eager loading tells the browser the image exists early. It does not tell the browser the image matters. By default, Chrome assigns images a low initial priority until layout confirms they are in the viewport, which means your hero can queue behind stylesheets, fonts, and the first handful of app scripts. The fetchpriority attribute fixes this by signaling importance before layout.
In Google's published tests, adding fetchpriority="high" to the LCP image reduced LCP by 5 to 30 percent, and in the Google Flights case study it cut LCP from 2.6 seconds to 1.9 seconds. On a Shopify store the gain is often at the upper end of that range because there is so much competing traffic in the first second of a page load.
Shopify's image_tag filter accepts fetchpriority directly:
{{ section.settings.image | image_url: width: 1600 | image_tag: loading: 'eager', fetchpriority: 'high', widths: '600, 900, 1200, 1600', sizes: '100vw' }}
Three rules keep this from backfiring. First, use fetchpriority="high" on one image per page. If everything is high priority, nothing is. Second, never combine it with loading="lazy"; the two instructions contradict each other and the lazy attribute wins. Third, consider fetchpriority="low" on below-the-fold images that would otherwise compete with the hero, such as a logo marquee or a "featured in" strip that sits just under the fold.
What about preload? Shopify's image_tag also accepts preload: true, which converts into a Link header. Preload and fetchpriority solve different problems. Preload helps the browser discover a resource it would otherwise find too late, for instance a CSS background image. Fetchpriority raises the priority of a resource the browser already knows about. If your hero is a proper img tag rendered by Liquid, you rarely need preload, and Shopify's guidance is to use it sparingly, because every preload pushes something else down the queue.
Fix 3: Explicit sizes and widths, no JavaScript guessing
A responsive image with a srcset is only useful if the browser can choose a candidate immediately. The sizes attribute is what makes that possible: it tells the browser how wide the image will render at each breakpoint, so it can pick the smallest adequate file before CSS has loaded.
Many lazy loading libraries, and a surprising number of themes, use sizes="auto" or compute sizes with JavaScript after the page renders. For a below-the-fold image this is harmless. For the LCP image it is a hidden delay, because the browser cannot start the download until the script has measured the layout. Always write an explicit sizes value for anything in the initial viewport.
Good defaults for a full-bleed hero: sizes: '100vw'. For a contained hero with a max width: sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'. For a four-up product grid: sizes: '(min-width: 1200px) calc(25vw - 2rem), (min-width: 768px) calc(33vw - 2rem), calc(50vw - 2rem)'. Pair these with a widths list that covers real device widths at 1x and 2x density.
Fix 4: Right-size through image_url and let the Shopify CDN work
The Shopify CDN is good at what it does. The image_url filter resizes on the fly, serves WebP or AVIF to browsers that accept them, and caches aggressively at the edge. The mistakes happen upstream of the CDN, in how the theme requests the image.
The most common one is requesting a single huge width. A hero rendered with image_url: width: 3000 and no widths list sends a multi-megabyte file to every device. The CDN cannot help if the markup asks for the wrong thing. Match the largest width in your widths list to the largest size the image will actually render at, and let srcset handle the rest.
The second mistake is background images. A hero implemented as a CSS background-image is invisible to the preload scanner, cannot use srcset, and cannot take fetchpriority. Shopify's performance best practices specifically advise against background images for heroes. Convert them to a positioned img with object-fit: cover and you unlock every fix in this article at once.
Fix 5: Clear the path, so the image is not fighting your apps
You can do everything above correctly and still see a slow LCP, because the browser has a limited number of connections and a limited amount of bandwidth on a mobile network. Every script that loads in the head or early in the body is competing with your hero for both.
Shopify stores are unusually exposed here. A typical store runs 15 to 30 apps, and most of them inject a script tag on every page. Reviews widgets, upsell bars, chat, analytics, heatmaps, consent managers, and A/B testing tools all load early because their developers want to make sure they run. The hero image, which is the thing the shopper is actually waiting for, gets what is left. We wrote about how to audit and prune these in third-party apps and Shopify page speed.
The practical steps: uninstall apps you no longer use (their scripts often linger after the app is removed, so check your theme.liquid and app embeds). Defer anything that does not need to run before first paint. Move analytics and marketing tags into the Shopify customer events pixel sandbox, which loads them off the critical path. And check your web fonts, because a font that blocks text rendering can delay LCP even when the image itself is fast; our Shopify font optimization guide covers that path.
Beyond theme-level changes, the biggest remaining lever is where the HTML itself comes from. If the document takes 800 milliseconds to arrive from origin, the image cannot start loading until then no matter how well it is marked up. Edge delivery of the full HTML document, which is what Nostra's Edge Delivery Engine does, cuts time to first byte to tens of milliseconds and gives every downstream fix a head start. The LCP phases compound in your favor: faster TTFB, earlier discovery, higher priority, smaller file, less competition.
Frequently asked questions
How do I know which image is the LCP element on my Shopify store?
Record a page load in Chrome DevTools' Performance panel with mobile throttling, then open the Insights tab. It highlights the LCP element and breaks its timing into TTFB, load delay, load duration, and render delay. Check the homepage, a collection page, and a product page separately, since the LCP element differs by template and by device.
Does fetchpriority="high" work on Shopify themes?
Yes. Shopify's image_tag Liquid filter accepts a fetchpriority parameter, so you can write image_tag: loading: 'eager', fetchpriority: 'high' directly in your section file. Apply it to one image per page and never combine it with loading="lazy".
Should I lazy-load product images on Shopify collection pages?
Lazy-load everything except the first few cards. Shopify recommends using forloop.index to eager-load roughly the first four products and lazy-load the rest. The first row is the LCP candidate; the rest are below the fold and benefit from deferral.
Is preload better than fetchpriority for a Shopify hero image?
They solve different problems. Preload helps the browser discover an image it would otherwise find late, such as a CSS background. Fetchpriority raises the priority of an image the browser already sees in the HTML. For a standard Liquid-rendered img tag, fetchpriority="high" is usually sufficient and preload should be used sparingly.
Why is my Shopify LCP still slow after fixing the image?
Check the other LCP phases. A slow time to first byte means the HTML itself is arriving late, and the image cannot start until the HTML does. Render-blocking fonts, CSS, or heavy app scripts can also delay the paint even when the image file has downloaded. Edge delivery of the HTML and deferring third-party scripts are the usual next steps.
Fix the hero, then fix the delivery
Shopify LCP image optimization is one of the rare performance projects where a single afternoon of theme edits produces a visible, measurable result in field data within a few weeks. Remove lazy loading from the hero, add fetchpriority="high", write explicit sizes, right-size the request, and get your apps out of the way. Then look at where the HTML comes from. Nostra's Edge Delivery Engine serves your storefront from the edge so the first byte, and therefore the hero, arrives before your competitors' pages have even started. Book a demo and we will show you your own LCP trace before and after.