Optimizing Site Speed with Lazy-Loading Images in Liquid

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

On Shopify storefronts, images often make up 60–80% of the total payload. From hero banners and product galleries to blog headers and collection grids, every extra image request adds latency, delays the Largest Contentful Paint (LCP), and increases total blocking time. Lazy‑loading defers the download of non‑critical images until they approach the viewport, dramatically speeding up initial page rendering and conserving mobile data. This end‑to‑end guide shows you how to integrate native lazy‑loading into your Liquid templates—complete with responsive srcset, aspect‑ratio placeholders, conditional eager loading, and accessibility best practices—so your store feels instantaneous without sacrificing visual richness.

1. Performance Context

Metric / ConceptWhy It Matters
Largest Contentful Paint (LCP)Slow image downloads inflate LCP, hurting user perception and SEO.
Total Payload SizeImages dominate bytes transferred, especially on mobile.
Data SavingsMobile users save bandwidth by only downloading images they view.

Key Principle:

Eager‑load only above‑the‑fold (hero, logo, first row of products).
Lazy‑load everything else.

2. Native vs. Script‑Based Lazy‑Loading

Add loading="lazy" to <img>:

liquidCopyEdit<img
  src="{{ image | image_url: width: 600 }}"
  alt="{{ image.alt | escape }}"
  loading="lazy"
>

Pros: No JS required; built‑in browser heuristics.
Cons: Very old browsers (<2020) lack support, but share is negligible.

2.2 Fetch Priority for Critical Images

For your hero or logo—critical for LCP—use:

liquidCopyEdit<img
  src="{{ section.settings.hero_image | image_url: width: 1600 }}"
  alt="{{ section.settings.hero_image.alt | escape }}"
  loading="eager"
  fetchpriority="high"
>
  • loading="eager" or omit (default).
  • fetchpriority="high" hint for supporting browsers.

2.3 IntersectionObserver Fallback (Optional)

For legacy support or advanced blur‑up patterns:

htmlCopyEdit<img
  data-src="full.jpg"
  src="placeholder.jpg"
  class="lazyload blur-up"
  alt="…">
<script>
  if ('IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries, obs) => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) return;
        const img = entry.target;
        img.src = img.dataset.src;
        img.onload = () => img.classList.add('loaded');
        obs.unobserve(img);
      });
    }, { rootMargin: '200px' });
    document.querySelectorAll('img.lazyload').forEach(img => io.observe(img));
  }
</script>

3. Preparing Responsive Images in Liquid

3.1 Resizing via image_url

Shopify’s CDN resizes on the fly:

liquidCopyEdit{%- assign img = product.featured_image -%}
<img
  src="{{ img | image_url: width: 800 }}"
  alt="{{ img.alt | escape }}"
  width="{{ img.width }}"
  height="{{ img.height }}"
  loading="lazy"
>
  • Intrinsic width/height attributes reserve layout space and prevent CLS.

3.2 Responsive srcset & sizes

Let the browser choose the optimal variant:

liquidCopyEdit{%- assign img = product.featured_image -%}
<img
  src="{{ img | image_url: width: 600 }}"
  srcset="
    {{ img | image_url: width: 300 }} 300w,
    {{ img | image_url: width: 600 }} 600w,
    {{ img | image_url: width: 900 }} 900w,
    {{ img | image_url: width: 1200 }} 1200w
  "
  sizes="(max-width: 600px) 100vw, 600px"
  alt="{{ img.alt | escape }}"
  loading="lazy"
>
  • sizes: maps viewport widths to image display size.

4. Conditional Lazy‑Loading in Liquid

4.1 Eager First Row in a Product Grid

Load only the first N images eagerly:

liquidCopyEdit<ul class="product-grid">
  {%- for product in collection.products -%}
    {%- assign img = product.featured_image -%}
    <li>
      <img
        src="{{ img | image_url: width: 400 }}"
        alt="{{ product.title | escape }}"
        width="{{ img.width }}"
        height="{{ img.height }}"
        {% if forloop.index <= 4 %}
          loading="eager" fetchpriority="high"
        {% else %}
          loading="lazy"
        {% endif %}
      >
    </li>
  {%- endfor -%}
</ul>

4.2 Merchant Toggle via Theme Setting

Expose a toggle in schema:

jsoncCopyEdit{
  "type": "checkbox",
  "id": "lazy_images",
  "label": "Enable lazy-loading images"
}

Use in Liquid:

liquidCopyEdit{% assign lazy_attr = section.settings.lazy_images | default: true | ternary: 'lazy', 'eager' %}
<img
  src="{{ img | image_url: width: 800 }}"
  alt="{{ img.alt | escape }}"
  loading="{{ lazy_attr }}"
>

5. Preventing Layout Shifts

5.1 Intrinsic Dimensions

Always output width and height:

liquidCopyEdit<img
  src="{{ img | image_url: width: 600 }}"
  width="{{ img.width }}"
  height="{{ img.height }}"
  loading="lazy"
>

5.2 Aspect‑Ratio Box

For background or decorative images:

liquidCopyEdit{%- assign ratio = img.aspect_ratio | default: 1.0 -%}
<div style="position:relative;padding-top:{{ 100.0 | divided_by: ratio }}%;">
  <img
    src="{{ img | image_url: width: 600 }}"
    alt="{{ img.alt | escape }}"
    loading="lazy"
    style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;"
  >
</div>

6. Low‑Quality Placeholder & Blur‑Up

Enhance perceived performance with a tiny blurred preview:

6.1 Generate Placeholder

liquidCopyEdit{% assign placeholder = img | image_url: width: 20 %}
{% assign main_src = img | image_url: width: 800 %}
<div class="blur-up">
  <img
    src="{{ placeholder }}"
    data-src="{{ main_src }}"
    alt="{{ img.alt | escape }}"
    class="blur-up__img lazyload"
  >
</div>

6.2 CSS Transition

cssCopyEdit.blur-up__img {
  filter: blur(20px);
  transition: filter 400ms;
}
.blur-up__img.loaded {
  filter: blur(0);
}

Use JS from §2.3 to swap data-src and add .loaded.

7. Lazy‑Loading Background Images

For modules using background-image:

liquidCopyEdit<div
  class="banner banner--lazy"
  data-bg="{{ section.settings.bg_image | image_url: width: 1920 }}"
  role="img"
  aria-label="{{ section.settings.bg_image.alt | escape }}"
></div>
jsCopyEditconst io = new IntersectionObserver((entries, obs) => {
  entries.forEach(({ isIntersecting, target }) => {
    if (!isIntersecting) return;
    target.style.backgroundImage = `url("${target.dataset.bg}")`;
    obs.unobserve(target);
  });
}, { rootMargin: '300px' });
document.querySelectorAll('.banner--lazy').forEach(el => io.observe(el));

Provide a light CSS background to avoid flash.

8. Accessibility & SEO

  • alt Text: Always meaningful or empty (alt="") for decorative images.
  • Semantics: Use <img> for content; for decorative, use CSS backgrounds with aria-hidden="true".
  • No layout jumps: Pre‑define dimensions or aspect ratios.

9. Testing & Measurement

Test TypeTools / Techniques
Lab (Lighthouse)Compare LCP, Total Bytes, Requests before/after.
RUMCapture LCP and transferSize via Performance API.
CoverageChrome DevTools > Coverage to verify critical vs deferred.
Scroll SimulationThrottled (3G) network, scroll test for lazy loads.

10. Reusable lazy-image Snippet

Create snippets/lazy-image.liquid:

liquidCopyEdit{%- assign img = include.image -%}
{%- assign w = include.max_width | default: img.width -%}
{%- assign eager = include.eager | default: false -%}
<img
  src="{{ img | image_url: width: w }}"
  srcset="
    {{ img | image_url: width: w }} {{ w }}w,
    {{ img | image_url: width: w | times: 2 }} {{ w | times: 2 }}w
  "
  sizes="{{ include.sizes | default: '100vw' }}"
  alt="{{ img.alt | escape }}"
  width="{{ img.width }}"
  height="{{ img.height }}"
  loading="{{ eager | ternary: 'eager', 'lazy' }}"
  {% if eager %} fetchpriority="high" {% endif %}
  class="{{ include.class | default: '' }}"
>

Use anywhere:

liquidCopyEdit{% render 'lazy-image',
  image: product.featured_image,
  max_width: 800,
  eager: forloop.index <= 4,
  class: 'product-img',
  sizes: '(max-width: 600px) 100vw, 400px' %}

Conclusion

By eager‑loading only critical above‑the‑fold images and lazy‑loading the rest:

  • LCP improves as hero assets arrive unblocked.
  • Bandwidth is conserved for mobile users.
  • Perceived speed climbs with faster initial paint.

Start by enabling loading="lazy" on grid thumbnails below the first row. Measure the impact, then expand to carousels, blog posts, and background modules—using the reusable snippet above. Within a few iterations, your Shopify store will feel snappier, rank higher on performance metrics, and turn more browsers into buyers.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts