Skip to main content

Never lazy-load the LCP image

Eagerly load images visible in the initial viewport. Never apply loading="lazy" or use a JavaScript-based lazy loader on the primary LCP target or other critical image in the initial viewport.


Lazy-loading the LCP image is one of the most common and damaging performance anti-patterns in Shopify themes. Server-side rendering with image_tag gives the browser maximum lead time on the LCP image, but only when the image is loaded eagerly.

When Liquid renders an img tag with a src attribute into the HTML, the browser's preload scanner discovers the image URL while parsing the HTML, before any CSS has been parsed, any JavaScript has run, or layout has begun. This gives the browser a head start on downloading the image as early as possible in the page load.

There are two related anti-patterns. First, loading="lazy" on the LCP candidate deprioritizes it and defers the download until after the browser processes a hidden IntersectionObserver event. Second, JavaScript lazy loading libraries like lazysizes replace the src attribute with a non-standard data-src, which hides the URL from the preload scanner entirely until JavaScript executes. Both practices are common, but they negate the benefits of the preload scanner and add latency.


Anchor to Remove ,[object Object], from the LCP imageRemove loading="lazy" from the LCP image

Change loading="lazy" to loading="eager", or remove the attribute. Eager loading is the default browser behavior.

{{ section.settings.image | image_url: width: 1000 | image_tag: loading: 'eager' }}

Anchor to Remove JavaScript lazy loading librariesRemove JavaScript lazy loading libraries

Libraries like lazysizes, lozad, and vanilla-lazyload replace src with data-src, hiding the image URL from the preload scanner until JavaScript executes.

To migrate:

  1. Replace data-src with src on all images.
  2. Add loading="lazy" to images outside the initial viewport.
  3. Remove the library script tag.

Anchor to Use ,[object Object], for conditional loadingUse section.index for conditional loading

The image_tag filter automatically applies loading="eager" for sections 1 through 3, and loading="lazy" for sections 4 and later. It also applies loading="eager" whenever section.index0 is nil, which is the case in the online store editor, in static sections, and in the Section Rendering API. Don't set the loading attribute when relying on this default.

When you override the default with section.index, match that nil handling. Comparisons against nil are falsey in Liquid, so {% if section.index <= 3 %} sends the LCP image to the else branch in exactly the contexts where the built-in default keeps it eager. Test the lazy case as a positive section.index > 3 comparison instead, and let everything else, including nil, fall through to eager. For the full explanation, refer to Load above-the-fold and below-the-fold sections differently with section.index:

{% if section.index == 1 %}
{%- comment -%} First section: eager, and the LCP candidate {%- endcomment -%}
{{ image
| image_url: width: 1000
| image_tag:
loading: 'eager',
fetchpriority: 'high'
}}
{% elsif section.index > 3 %}
{%- comment -%} Sections 4 and later {%- endcomment -%}
{{ image
| image_url: width: 1000
| image_tag: loading: 'lazy'
}}
{% else %}
{%- comment -%} Sections 2 and 3, and any context where section.index is nil {%- endcomment -%}
{{ image
| image_url: width: 1000
| image_tag: loading: 'eager'
}}
{% endif %}

Anchor to Use ,[object Object], for image-dense sectionsUse forloop.index for image-dense sections

A collection section might render up to 50 products at a time, all within the first section. section.index alone isn't sufficient here, because only the first few images are visible in the viewport and are LCP candidates. Use forloop.index to enable eager loading for just those cards, and leave the rest to be lazy loaded:

{% for product in collection.products %}
{%- liquid
if forloop.index <= 4
assign image_loading = 'eager'
else
assign image_loading = 'lazy'
endif
-%}
{{ product.featured_image
| image_url: width: 400
| image_tag:
loading: image_loading,
widths: '200, 300, 400',
sizes: '(min-width: 1200px) calc(25vw - 2rem), (min-width: 768px) calc(33vw - 2rem), calc(50vw - 2rem)'
}}
{% endfor %}

Anchor to Avoid auto-sizes on LCP imagesAvoid auto-sizes on LCP images

Calculating sizes with JavaScript, as some lazy loading libraries do, delays image download because the browser can't select the correct source until after JavaScript runs. Always provide an explicit sizes attribute for images visible in the initial viewport:

{{ image
| image_url: width: 1000
| image_tag:
loading: 'eager',
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}

Anchor to Example 1. Hero or banner sectionsExample 1. Hero or banner sections

{%- comment -%} Before: always lazy loads, even when this section is at the top of the page {%- endcomment -%}
{{ section.settings.image
| image_url: width: 1600
| image_tag:
loading: 'lazy',
widths: '600, 900, 1200, 1600',
sizes: '100vw'
}}

{%- comment -%} After: section.index determines loading at render time, and nil stays eager {%- endcomment -%}
{%- liquid
assign hero_loading = 'eager'
assign hero_priority = 'auto'
if section.index == 1
assign hero_priority = 'high'
elsif section.index > 3
assign hero_loading = 'lazy'
endif
-%}
{{ section.settings.image
| image_url: width: 1600
| image_tag:
loading: hero_loading,
fetchpriority: hero_priority,
widths: '600, 900, 1200, 1600',
sizes: '100vw'
}}

Anchor to Example 2. Migrate from lazysizesExample 2. Migrate from lazysizes

Before, with lazysizes:

<script src="{{ 'lazysizes.min.js' | asset_url }}" async></script>
<img
data-src="{{ section.settings.image | image_url: width: 1200 }}"
data-srcset="{{ section.settings.image | image_url: width: 400 }} 400w,
{{ section.settings.image | image_url: width: 800 }} 800w,
{{ section.settings.image | image_url: width: 1200 }} 1200w"
data-sizes="auto"
class="lazyload"
alt="{{ section.settings.image.alt | escape }}"
/>

After, with image_tag. Remove the script tag from your layout file and provide an explicit sizes attribute, because you can no longer use data-sizes="auto":

{{ section.settings.image
| image_url: width: 1200
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '400, 800, 1200',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}

Anchor to Example 3. Product card grid with ,[object Object]Example 3. Product card grid with forloop.index

{% for product in collection.products %}
{%- comment -%} Eagerly load only the first few items {%- endcomment -%}
{%- liquid
if forloop.index <= 4
assign image_loading = 'eager'
else
assign image_loading = 'lazy'
endif
-%}
{{ product.featured_image
| image_url: width: 400
| image_tag:
loading: image_loading,
widths: '200, 300, 400',
sizes: '(min-width: 1200px) calc(25vw - 2rem), (min-width: 768px) calc(33vw - 2rem), calc(50vw - 2rem)'
}}
{% endfor %}

  • Use the Insights tab in the Performance panel to identify whether the LCP image is being lazy loaded.
  • Use the Network panel to check when the LCP image request starts. It should appear early in the waterfall, discovered by the preload scanner, not after JavaScript executes.
  • Compare Performance panel runs with loading="lazy" and loading="eager" on the LCP image to see the LCP difference.


Was this page helpful?