Lazy loading is the most misapplied fix in Shopify performance work. In most stores we audit, removing it improves LCP more than adding it. Developers either pile lazy loading on top of what the theme already does, or apply it to the hero image, making LCP worse.
The fix is not more lazy loading. It is precision. Load the right things first, defer the rest.
This article shows exactly which images and scripts to target, the Liquid patterns from a real Shero theme, and how to verify the result without breaking anything.
What Shopify already lazy loads (and what it doesn’t)
Modern Shopify themes built on Online Store 2.0, including Dawn, Craft, and Sense, apply the loading=“lazy” attribute to images by default, using the browser’s native lazy-loading feature. There is no plugin, no JavaScript library, and no extra configuration required. When a merchant installs one of these themes out of the box, below-the-fold images are already deferred.
What this means in practice: if you are working on a standard OS 2.0 theme and you add loading=“lazy” to images that do not already have it, you are likely duplicating work that is already done.

The fold line is the dividing rule. Everything above it needs loading="eager" and fetchpriority="high" on the LCP element. Everything below it is safe to defer with loading="lazy
Where the gaps are
The gap is conditional logic. Dawn and most OS 2.0 themes apply loading=“lazy” as a default, but they do not always detect whether a given section is above or below the fold.
A merchant can drag the image banner section into the first position in the theme editor, and that section may still carry loading=“lazy” on its image because the theme does not know it is now the first thing on the page.
The result is that the most important image on the page, the one that determines your LCP score, gets deferred.
The LCP trap: why lazy loading can hurt your score
Before making any changes, you need to know which element the browser is treating as your Largest Contentful Paint. Changing the wrong image makes no difference. Changing the right one can move your LCP score by several seconds.
How to identify your LCP element
Open Chrome, navigate to your store in an incognito window, and open DevTools. Go to the Performance tab, click the record button, reload the page, and stop the recording.
In the Insights panel on the left, you will see LCP with a time value and a breakdown showing Time to first byte and Element render delay.

The LCP breakdown shows time to first byte at 40ms and element render delay at 234ms. If your element render delay is high, the image is likely being deferred when it should be loading eagerly.
Alternatively, run a Lighthouse audit directly in DevTools by clicking the Lighthouse tab and generating a report.

Lighthouse audit in DevTools showing a performance score of 70 and LCP at 3.9s. An LCP this high on a text-based hero site is almost always a deferred-image problem. That is a site where lazy loading hurts more than it helps.
What to lazy load and what not to
Not every image on a Shopify store should be treated the same way. The decision comes down to one question.
Is this element visible when the page first loads? If yes, load it eagerly. If no, defer it.
The table below maps the most common elements to the appropriate strategy, based on Shopify’s best practices for lazy loading.
|
Element |
Load Strategy |
Reason |
|
Hero/banner image (first section) |
eager + fetchpriority="high" |
Almost always, the LCP element is on the homepage and landing pages |
|
Second hero slide or block |
lazy |
Not visible on initial load |
|
Blog card images |
lazy |
Always below the fold on listing pages |
|
Product card images (collection grid) |
lazy |
Banner above the grid owns LCP in most cases |
|
First product card (no banner above grid) |
eager |
May own LCP if nothing above it |
|
Logo in header |
eager |
Visible on every page load, but keep small and optimized |
|
Nav/menu images |
eager |
Visible on load, typically lightweight |
|
Review widget images |
lazy |
Never rendered above fold |
|
Footer images |
lazy |
Never above fold |
For a broader look at how lazy loading fits into your overall image strategy, see our Shopify image optimization guide
The fix: eager loading and fetchpriority=“high” on the hero
Once you have confirmed which element is your LCP, the fix is two attributes on the image tag: loading=“eager” and fetchpriority=“high”.
Loading eager tells the browser not to defer this image. Fetchpriority high tells the browser to download it before other resources on the page. Used together on the LCP image, these two attributes are the single highest-impact change you can make to LCP.
There are two ways to implement this, depending on how the section is built.
-
If the image is rendered using a raw HTML img tag, add the attributes directly.
-
If it uses Shopify’s Liquid image_tag filter, pass them as parameters to the filter. Both approaches produce the same output HTML.
The loading attribute below is set conditionally using an eager_block_id variable: if the block ID matches the designated eager block, loading is set to eager; otherwise, it falls back to lazy.

The highlighted line 60 shows loading=“{% if block.id == eager_block_id %}eager{% else %}lazy{% endif %}”. This is a valid approach, but it requires the developer to manually designate which block gets eager loading.
Now let’s take a more solid approach. The conditional has been simplified to forloop.first: if this is the first block in the loop, apply loading=“eager” and fetchpriority=“high”; otherwise, apply loading=“lazy”.

cards-shero-hero.liquid with forloop.first conditional on line 43. The first image in the block loop gets loading=“eager” and fetchpriority=“high”; all subsequent images get loading=“lazy”.
Using section.index to conditionally apply fetchpriority
The forloop.first approach works well within a section that renders multiple blocks. But there is a more precise method for sections that render a single hero image: using section.index to detect whether the current section is the first section on the page.
Section.index is a Liquid variable that returns the position of the current section in the template. If section.index equals 1, this section is first. That means its image is almost certainly above the fold, regardless of which template it appears in or how the merchant has arranged sections in the theme editor.
The screenshot below shows this pattern in hero.liquid. The section assigns fetch_priority = ‘auto’ as the default, then overrides it to ‘high’ if section.index == 1. The fetch_priority variable is then passed to the image tag further down in the template. This is the cleanest and most portable implementation: it works correctly whether the hero section is first, second, or third on any given page.

hero.liquid lines 110-114. fetch_priority defaults to ‘auto’ and is overridden to ‘high’ only when section.index == 1. This ensures fetchpriority is applied precisely to the first section on the page without hardcoding it to a specific section name.
How to lazy load images below the fold in Liquid
Once you have protected the LCP image from lazy loading, you can be confident about applying loading=“lazy” to everything else. The correct pattern is to set the attribute directly on the img tag for any image that is not visible on page load.
Notice that the full context is shown, not just the attribute in isolation. The image tag includes src, srcset with multiple widths, sizes for responsive breakpoints, alt text, width, and height. The loading attribute sits alongside these, not instead of them. A complete, well-formed image tag with lazy loading is what you are aiming for.

_blog-post-card.liquid showing loading="lazy" on both the article image (line 69) and the placeholder fallback (line 81).
The above approach is correct because blog card images are always below the fold on a listing page: they appear in a grid that the user scrolls to, not at the top of the viewport.
Applying it in a product card loop
Product card images require slightly more thought than blog images because the first row of cards on a collection page is visible on page load. Lazy loading the first row defers images that the browser would have loaded immediately anyway, which can cause a flash of empty space as the user arrives on the page.
The correct approach is to lazy load all product images by default, but use additional logic to handle the first visible items differently if needed.

card-gallery.liquid lines 109 to 122. The loading variable is assigned ‘lazy’ at line 111 as the default for all product media in the loop. Variant image logic and slide count limiting follow below.
In practice, the first row of product cards on a collection page rarely constitutes the LCP element (typically the banner), so lazy-loading them is acceptable. If PSI flags the collection page product images as a problem, the fix is to apply the forloop.first pattern here too, setting eager on the first iteration and lazy on the rest.
Lazy loading and INP: it’s about scripts, not images
You've protected the LCP image and deferred everything below the fold. That handles the visual load. But if your store still feels sluggish after the page renders, the problem has shifted from images to scripts.
INP measures the latency of every interaction throughout the page session: clicks, taps, keyboard input. On most Shopify stores, the number is worse than it needs to be, and images are not the reason.
What causes poor INP on Shopify stores
The primary cause of poor INP on Shopify stores is not images. It is third-party scripts running synchronously on the main thread.

Our Speed benchmarks across 1,000 Shopify stores found INP is the second biggest drag on the composite score.
When the browser is executing JavaScript, it cannot respond to user interactions. Every millisecond spent running a chat widget, review app, tracking pixel, or analytics script is a millisecond the page cannot react to a button press or menu tap.
The most common offenders are Google Tag Manager loaded synchronously, live chat scripts (Gorgias, Intercom, Tidio), loyalty and rewards apps, and heavy review widgets like Yotpo or Stamped. None of these need to be available the instant the page loads. They can all be deferred without any meaningful impact on the user experience.

On this Shopify store, the GTM tag added 430.9 KiB of network payload for a Lighthouse Performance score of 91.
The table below shows some of the causes/fixes we notice on the Shopify store.
|
If Your Store Has |
The Fix |
Expected Improvement |
|
Klaviyo, Facebook Pixel, or Google Analytics |
Defer scripts |
100-200ms |
|
Product variants (sizes, colors) |
Optimize variant selector |
150-300ms |
|
Review apps (Judge.me, Loox, Yotpo) |
Lazy load reviews |
50-100ms |
|
Collection pages with 48+ products |
Lazy load images |
100-200ms |
|
Mega menu with lots of links |
Load menu on hover |
80-150ms |
Read the full guide on Shopify INP Optimization to find the exact fix for your store
Defer, async, and load-on-interaction patterns
There are three ways to stop a third-party script from blocking the main thread.
-
The async attribute tells the browser to download the script in parallel and execute it as soon as it is ready, without waiting for HTML parsing to finish.
-
The defer attribute does the same, but delays execution until after the HTML is fully parsed and preserves the order of execution if multiple deferred scripts are present. For most third-party app scripts, defer is the safer choice.
-
The load-on-interaction is the most aggressive option. The script does not load at all until the user performs a qualifying action such as scrolling, moving the mouse, touching the screen, or pressing a key. This can dramatically reduce initial page weight and improve both LCP and INP, at the cost of a brief delay before the widget is ready.
In theme.liquid, Google Tag Manager is loaded synchronously: a script element is created, the GTM URL is assigned to its src, and it is immediately appended to the document head. This blocks HTML parsing until GTM has downloaded and executed.
theme.liquid lines 9 to 17. GTM is loaded synchronously. A script element is created and immediately appended to the document head on line 15, blocking the main thread while GTM downloads and initializes.

theme.liquid lines 9 to 41. GTM loads only after the first user interaction, with a requestIdleCallback fallback at 3,000ms.
How to test without breaking your theme
Run PageSpeed Insights at pagespeed.web.dev and enter your store URL. Run the mobile test first: mobile scores are typically lower and represent a more realistic worst-case scenario for your customers.

PageSpeed Insights mobile is showing a performance score of 50, LCP at 8.0s, and Total Blocking Time at 480ms. LCP this high on a Shopify store is almost always caused by a lazily loaded hero image or an unoptimized LCP element.
The second screenshot shows the same site after applying the fixes: converting the hero image to eager loading with fetchpriority=“high”, and deferring third-party scripts.

After applying the fixes, PageSpeed Insights mobile is showing a performance score of 78, an LCP of 4.0s, and a Total Blocking Time of 230ms. The hero image change and script deferral together account for the majority of this improvement.
The one check you must do every time
After any change to image-loading attributes, rerun the Performance tab recording in Chrome DevTools and confirm that the LCP element is not lazy-loaded. This is the single most important verification step. One missed loading=“lazy” on the hero image will undo all other work.
Do not rely solely on PSI scores for this check. PSI uses a simulated test environment and scores can vary between runs. DevTools gives you direct visibility into what the browser is actually doing during a real page load on your machine.
Common mistakes to avoid
Keep a checklist of these before marking any performance work as complete.
-
Lazy loading the hero or banner image. Remove loading=“lazy” from any image that is visible on page load. Add loading=“eager” and fetchpriority=“high”. Verify in DevTools after every change.
-
Dawn’s image banner section carrying loading=“lazy” when placed first. In sections/image-banner.liquid, check for loading=“lazy” and add section.index == 1 logic to override it to eager when the section is first.
-
Applying fetchpriority=“high” to multiple images. The browser ignores the hint when everything is marked high priority. Use fetchpriority=“high” on one element per page only, and let the browser prioritize everything else naturally.
-
Using a third-party lazy load JS library on top of native browser lazy loading. Modern browsers handle this natively. Adding a library like lazysizes creates conflict risk, adds script weight, and solves a problem that does not exist on OS 2.0 themes.
-
Not verifying the LCP element after changes. Run DevTools after every image attribute change. PSI scores are a lagging indicator. DevTools shows you what is actually happening on the page right now.
Conclusion and next steps
Getting lazy loading right is less about knowing the attributes and more about knowing where to apply them. The same applies to all your Shopify performance optimization efforts.
If your Shopify store is slow, we can tell you exactly why.
Our experts audit theme performance at the code level: LCP element identification, image loading logic, script deferral, and Core Web Vitals across your key page templates. No generic recommendations, no automated reports.
Book a performance audit here