Platform Guide 28 min read

Shopify Image Optimization Bible

Complete guide to image optimization in Shopify. Master Liquid image filters, theme development, product images, and performance optimization for faster stores.

By ImageGuide Team · Published January 19, 2026 · Updated January 19, 2026
shopifyliquide-commerceproduct imagestheme development

Shopify handles image optimization automatically through its CDN, but understanding how to leverage its features properly can significantly improve your store’s performance. This guide covers everything from basic Liquid filters to advanced optimization techniques.

Shopify Image Fundamentals

How Shopify Handles Images

Shopify automatically:

  • Serves images from its global CDN
  • Generates multiple sizes on-the-fly
  • Converts to WebP for supporting browsers
  • Caches optimized versions

Image URL Structure

https://cdn.shopify.com/s/files/1/0000/0001/files/
  product-image.jpg
  ?v=1234567890           # Cache busting version
  &width=400              # Requested width
  &crop=center            # Crop position

Available Parameters

ParameterOptionsDescription
width1-5760Target width in pixels
height1-5760Target height in pixels
croptop, center, bottom, left, rightCrop position
formatjpg, png, pjpg, webpOutput format
pad_colorhex colorBackground for padding

Liquid Image Filters

The image_url Filter

Modern Shopify’s primary image filter:

{{ product.featured_image | image_url: width: 800 }}

Output:

//cdn.shopify.com/s/files/1/0000/0001/files/product.jpg?v=123&width=800

Common Parameters

{%- comment -%} Basic resize {%- endcomment -%}
{{ image | image_url: width: 400 }}
{{ image | image_url: height: 300 }}

{%- comment -%} Fixed dimensions with crop {%- endcomment -%}
{{ image | image_url: width: 400, height: 400, crop: 'center' }}

{%- comment -%} Force format {%- endcomment -%}
{{ image | image_url: width: 800, format: 'pjpg' }}

The image_tag Filter

Generates complete img element:

{{ product.featured_image | image_url: width: 800 | image_tag }}

Output:

<img src="//cdn.shopify.com/.../product.jpg?width=800"
     alt=""
     width="800"
     height="600">

Customizing image_tag

{{
  product.featured_image |
  image_url: width: 800 |
  image_tag:
    loading: 'lazy',
    class: 'product-image',
    alt: product.title
}}

Responsive Images

Basic Srcset

{%- assign widths = '165,360,535,750,1000,1500' | split: ',' -%}

<img
  src="{{ image | image_url: width: 750 }}"
  srcset="{%- for width in widths -%}
    {{ image | image_url: width: width }} {{ width }}w{% unless forloop.last %}, {% endunless %}
  {%- endfor -%}"
  sizes="(min-width: 1200px) 50vw, 100vw"
  alt="{{ image.alt | escape }}"
  width="{{ image.width }}"
  height="{{ image.height }}"
  loading="lazy"
>

Responsive Image Snippet

Create snippets/responsive-image.liquid:

{%- comment -%}
  Responsive image snippet
  Usage: {% render 'responsive-image', image: product.featured_image, sizes: '50vw' %}
{%- endcomment -%}

{%- liquid
  assign widths = '165,360,535,750,1000,1500,2000' | split: ','
  assign default_width = 750
  assign loading = loading | default: 'lazy'
  assign fetchpriority = fetchpriority | default: nil
-%}

<img
  src="{{ image | image_url: width: default_width }}"
  srcset="{%- for width in widths -%}
    {{ image | image_url: width: width }} {{ width }}w{% unless forloop.last %}, {% endunless %}
  {%- endfor -%}"
  sizes="{{ sizes | default: '100vw' }}"
  alt="{{ image.alt | escape }}"
  width="{{ image.width }}"
  height="{{ image.height }}"
  loading="{{ loading }}"
  {%- if fetchpriority %}
  fetchpriority="{{ fetchpriority }}"
  {%- endif %}
  {%- if class %}
  class="{{ class }}"
  {%- endif %}
>

Usage Examples

{%- comment -%} Hero image - eager load {%- endcomment -%}
{% render 'responsive-image',
  image: section.settings.hero_image,
  sizes: '100vw',
  loading: 'eager',
  fetchpriority: 'high'
%}

{%- comment -%} Product grid - lazy load {%- endcomment -%}
{% render 'responsive-image',
  image: product.featured_image,
  sizes: '(min-width: 1200px) 25vw, (min-width: 768px) 50vw, 100vw',
  loading: 'lazy'
%}

Dawn Theme Patterns

Shopify’s Dawn theme uses modern image patterns.

Dawn’s Image Component

{%- comment -%} Based on Dawn's image rendering {%- endcomment -%}
{%- liquid
  assign image_width = section.settings.image_width
  assign aspect_ratio = section.settings.aspect_ratio

  case aspect_ratio
    when 'adapt'
      assign aspect_ratio_value = image.aspect_ratio
    when 'square'
      assign aspect_ratio_value = 1
    when 'portrait'
      assign aspect_ratio_value = 0.75
    when 'landscape'
      assign aspect_ratio_value = 1.5
  endcase
-%}

<div
  class="media"
  style="--aspect-ratio: {{ aspect_ratio_value }};"
>
  {{ image | image_url: width: image_width | image_tag:
    loading: 'lazy',
    sizes: sizes,
    widths: '165, 360, 535, 750, 1000, 1500',
    class: 'motion-reduce'
  }}
</div>

Dawn’s Lazy Loading Pattern

{%- if section.index == 1 -%}
  {%- assign lazy_load = false -%}
{%- else -%}
  {%- assign lazy_load = true -%}
{%- endif -%}

{{ image | image_url: width: 800 | image_tag:
  loading: lazy_load | ternary: 'lazy', 'eager',
  fetchpriority: lazy_load | ternary: nil, 'high'
}}

Product Images

<div class="product-gallery">
  {%- comment -%} Main image {%- endcomment -%}
  <div class="product-main-image">
    {% render 'responsive-image',
      image: product.featured_image,
      sizes: '(min-width: 1200px) 50vw, 100vw',
      loading: 'eager',
      fetchpriority: 'high',
      class: 'product-featured'
    %}
  </div>

  {%- comment -%} Thumbnails {%- endcomment -%}
  <div class="product-thumbnails">
    {%- for image in product.images -%}
      <button
        type="button"
        class="thumbnail{% if forloop.first %} active{% endif %}"
        data-image-url="{{ image | image_url: width: 1000 }}"
        data-image-srcset="{%- for w in widths -%}{{ image | image_url: width: w }} {{ w }}w{% unless forloop.last %},{% endunless %}{%- endfor -%}"
      >
        <img
          src="{{ image | image_url: width: 100 }}"
          alt="{{ image.alt | escape | default: product.title }}"
          width="100"
          height="100"
          loading="lazy"
        >
      </button>
    {%- endfor -%}
  </div>
</div>

Variant Images

{%- comment -%} Map variant images for JavaScript {%- endcomment -%}
<script type="application/json" id="variant-images">
  {
    {%- for variant in product.variants -%}
      "{{ variant.id }}": {
        "src": "{{ variant.featured_image | image_url: width: 1000 }}",
        "srcset": "{%- for w in widths -%}{{ variant.featured_image | image_url: width: w }} {{ w }}w{% unless forloop.last %},{% endunless %}{%- endfor -%}",
        "alt": {{ variant.featured_image.alt | default: variant.title | json }}
      }{% unless forloop.last %},{% endunless %}
    {%- endfor -%}
  }
</script>

Zoom Support

<div
  class="product-zoom-container"
  data-zoom-image="{{ product.featured_image | image_url: width: 2000 }}"
>
  <img
    src="{{ product.featured_image | image_url: width: 800 }}"
    srcset="{{ product.featured_image | image_url: width: 400 }} 400w,
            {{ product.featured_image | image_url: width: 800 }} 800w,
            {{ product.featured_image | image_url: width: 1200 }} 1200w"
    sizes="(min-width: 1200px) 50vw, 100vw"
    alt="{{ product.featured_image.alt | escape }}"
    loading="eager"
  >
</div>

Collection Pages

Product Grid

<div class="collection-grid">
  {%- for product in collection.products -%}
    <article class="product-card">
      <a href="{{ product.url }}">
        {%- if product.featured_image -%}
          <img
            src="{{ product.featured_image | image_url: width: 400 }}"
            srcset="{{ product.featured_image | image_url: width: 200 }} 200w,
                    {{ product.featured_image | image_url: width: 300 }} 300w,
                    {{ product.featured_image | image_url: width: 400 }} 400w,
                    {{ product.featured_image | image_url: width: 600 }} 600w"
            sizes="(min-width: 1200px) 20vw, (min-width: 768px) 33vw, 50vw"
            alt="{{ product.featured_image.alt | escape | default: product.title }}"
            width="{{ product.featured_image.width }}"
            height="{{ product.featured_image.height }}"
            loading="{% if forloop.index <= 4 %}eager{% else %}lazy{% endif %}"
          >
        {%- else -%}
          {%- comment -%} Placeholder {%- endcomment -%}
          {{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}
        {%- endif -%}
      </a>
      <h3>{{ product.title }}</h3>
      <p>{{ product.price | money }}</p>
    </article>
  {%- endfor -%}
</div>

Hover Image Swap

<div class="product-card-images">
  {%- if product.images.size > 1 -%}
    <img
      class="product-card-image primary"
      src="{{ product.featured_image | image_url: width: 400 }}"
      alt="{{ product.featured_image.alt | escape }}"
      loading="lazy"
    >
    <img
      class="product-card-image hover"
      src="{{ product.images[1] | image_url: width: 400 }}"
      alt="{{ product.images[1].alt | escape }}"
      loading="lazy"
    >
  {%- else -%}
    <img
      class="product-card-image"
      src="{{ product.featured_image | image_url: width: 400 }}"
      alt="{{ product.featured_image.alt | escape }}"
      loading="lazy"
    >
  {%- endif -%}
</div>
.product-card-images {
  position: relative;
}

.product-card-image.hover {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.product-card:hover .product-card-image.hover {
  opacity: 1;
}

Hero Section

{%- comment -%} sections/hero-banner.liquid {%- endcomment -%}
{%- liquid
  assign image = section.settings.image
  assign mobile_image = section.settings.mobile_image
-%}

<section class="hero-banner">
  <picture>
    {%- if mobile_image -%}
      <source
        media="(max-width: 749px)"
        srcset="{{ mobile_image | image_url: width: 375 }} 375w,
                {{ mobile_image | image_url: width: 550 }} 550w,
                {{ mobile_image | image_url: width: 750 }} 750w"
        sizes="100vw"
      >
    {%- endif -%}
    <source
      media="(min-width: 750px)"
      srcset="{{ image | image_url: width: 1000 }} 1000w,
              {{ image | image_url: width: 1500 }} 1500w,
              {{ image | image_url: width: 2000 }} 2000w,
              {{ image | image_url: width: 3000 }} 3000w"
      sizes="100vw"
    >
    <img
      src="{{ image | image_url: width: 1500 }}"
      alt="{{ image.alt | escape }}"
      width="{{ image.width }}"
      height="{{ image.height }}"
      loading="eager"
      fetchpriority="high"
    >
  </picture>

  <div class="hero-content">
    <h1>{{ section.settings.heading }}</h1>
  </div>
</section>

{% schema %}
{
  "name": "Hero Banner",
  "settings": [
    {
      "type": "image_picker",
      "id": "image",
      "label": "Desktop Image"
    },
    {
      "type": "image_picker",
      "id": "mobile_image",
      "label": "Mobile Image (optional)"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading"
    }
  ]
}
{% endschema %}

Background Images via CSS

{%- style -%}
  .hero-section {
    background-image: url('{{ section.settings.image | image_url: width: 1500 }}');
  }

  @media (min-width: 1500px) {
    .hero-section {
      background-image: url('{{ section.settings.image | image_url: width: 2500 }}');
    }
  }

  @media (max-width: 749px) {
    .hero-section {
      background-image: url('{{ section.settings.image | image_url: width: 750 }}');
    }
  }
{%- endstyle -%}

Theme Settings Images

Logo Optimization

{%- comment -%} In header.liquid {%- endcomment -%}
{%- if settings.logo -%}
  {%- assign logo_height = settings.logo_height | default: 50 -%}
  {%- assign logo_width = settings.logo.width | times: logo_height | divided_by: settings.logo.height -%}

  <img
    src="{{ settings.logo | image_url: height: logo_height }}"
    srcset="{{ settings.logo | image_url: height: logo_height }} 1x,
            {{ settings.logo | image_url: height: logo_height | times: 2 }} 2x"
    alt="{{ settings.logo.alt | default: shop.name }}"
    width="{{ logo_width }}"
    height="{{ logo_height }}"
    loading="eager"
  >
{%- else -%}
  <span class="header-logo-text">{{ shop.name }}</span>
{%- endif -%}

Favicon

{%- comment -%} In theme.liquid head {%- endcomment -%}
{%- if settings.favicon -%}
  <link rel="icon" type="image/png" href="{{ settings.favicon | image_url: width: 32, height: 32 }}">
  <link rel="apple-touch-icon" href="{{ settings.favicon | image_url: width: 180, height: 180 }}">
{%- endif -%}

Performance Optimization

Critical Image Preloading

{%- comment -%} In theme.liquid head {%- endcomment -%}
{%- if template.name == 'index' and section.settings.hero_image -%}
  <link
    rel="preload"
    as="image"
    href="{{ section.settings.hero_image | image_url: width: 1500 }}"
    imagesrcset="{{ section.settings.hero_image | image_url: width: 750 }} 750w,
                 {{ section.settings.hero_image | image_url: width: 1500 }} 1500w,
                 {{ section.settings.hero_image | image_url: width: 2500 }} 2500w"
    imagesizes="100vw"
  >
{%- endif -%}

LCP Optimization

{%- comment -%} Detect if section is above the fold {%- endcomment -%}
{%- liquid
  if section.index == 1
    assign loading = 'eager'
    assign fetchpriority = 'high'
  else
    assign loading = 'lazy'
    assign fetchpriority = nil
  endif
-%}

<img
  src="{{ image | image_url: width: 800 }}"
  loading="{{ loading }}"
  {% if fetchpriority %}fetchpriority="{{ fetchpriority }}"{% endif %}
>

Lazy Loading Collection Pages

{%- comment -%} Eager load first row, lazy load rest {%- endcomment -%}
{%- assign products_per_row = section.settings.products_per_row | default: 4 -%}

{%- for product in collection.products -%}
  {%- assign row = forloop.index | minus: 1 | divided_by: products_per_row -%}

  <img
    src="{{ product.featured_image | image_url: width: 400 }}"
    loading="{% if row == 0 %}eager{% else %}lazy{% endif %}"
    {% if row == 0 and forloop.index == 1 %}fetchpriority="high"{% endif %}
  >
{%- endfor -%}

Image Placeholders

Blur-Up Placeholder

{%- comment -%} Generate tiny placeholder {%- endcomment -%}
{%- assign placeholder_url = image | image_url: width: 20 -%}

<div class="image-wrapper" data-loaded="false">
  <img
    class="placeholder-image"
    src="{{ placeholder_url }}"
    alt=""
    aria-hidden="true"
  >
  <img
    class="main-image"
    src="{{ image | image_url: width: 800 }}"
    alt="{{ image.alt | escape }}"
    loading="lazy"
    onload="this.parentElement.dataset.loaded = 'true'"
  >
</div>

<style>
  .image-wrapper {
    position: relative;
    overflow: hidden;
  }

  .placeholder-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: blur(10px);
    transform: scale(1.1);
    transition: opacity 0.3s;
  }

  .image-wrapper[data-loaded="true"] .placeholder-image {
    opacity: 0;
  }

  .main-image {
    display: block;
    width: 100%;
  }
</style>

Skeleton Placeholder

<div class="image-skeleton">
  <img
    src="{{ image | image_url: width: 800 }}"
    alt="{{ image.alt | escape }}"
    loading="lazy"
    onload="this.parentElement.classList.add('loaded')"
  >
</div>

<style>
  .image-skeleton {
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
  }

  .image-skeleton.loaded {
    background: none;
    animation: none;
  }

  @keyframes shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }
</style>

Metafield Images

Accessing Metafield Images

{%- comment -%} File reference metafield {%- endcomment -%}
{%- assign custom_image = product.metafields.custom.lifestyle_image.value -%}

{%- if custom_image -%}
  <img
    src="{{ custom_image | image_url: width: 800 }}"
    alt="{{ custom_image.alt | escape }}"
    loading="lazy"
  >
{%- endif -%}

List of Images Metafield

{%- assign gallery_images = product.metafields.custom.gallery.value -%}

{%- if gallery_images.size > 0 -%}
  <div class="custom-gallery">
    {%- for image in gallery_images -%}
      <img
        src="{{ image | image_url: width: 400 }}"
        alt="{{ image.alt | escape }}"
        loading="lazy"
      >
    {%- endfor -%}
  </div>
{%- endif -%}

WebP and Format Optimization

Shopify automatically serves WebP when supported, but you can force formats:

Force Format

{%- comment -%} Force WebP {%- endcomment -%}
{{ image | image_url: width: 800, format: 'webp' }}

{%- comment -%} Force progressive JPEG {%- endcomment -%}
{{ image | image_url: width: 800, format: 'pjpg' }}

{%- comment -%} Force PNG (for transparency) {%- endcomment -%}
{{ image | image_url: width: 800, format: 'png' }}

Picture Element for Format Control

<picture>
  <source
    type="image/webp"
    srcset="{{ image | image_url: width: 400, format: 'webp' }} 400w,
            {{ image | image_url: width: 800, format: 'webp' }} 800w"
  >
  <img
    src="{{ image | image_url: width: 800 }}"
    srcset="{{ image | image_url: width: 400 }} 400w,
            {{ image | image_url: width: 800 }} 800w"
    alt="{{ image.alt | escape }}"
    loading="lazy"
  >
</picture>

Third-Party CDN Integration

Using Sirv with Shopify

{%- comment -%} Replace Shopify CDN with Sirv {%- endcomment -%}
{%- assign sirv_base = 'https://yourstore.sirv.com' -%}
{%- assign image_path = product.featured_image.src | split: 'files/' | last -%}

<img
  src="{{ sirv_base }}/{{ image_path }}?w=800&format=optimal"
  srcset="{{ sirv_base }}/{{ image_path }}?w=400&format=optimal 400w,
          {{ sirv_base }}/{{ image_path }}?w=800&format=optimal 800w,
          {{ sirv_base }}/{{ image_path }}?w=1200&format=optimal 1200w"
  alt="{{ product.featured_image.alt | escape }}"
  loading="lazy"
>

Cloudinary Integration

{%- assign cloudinary_base = 'https://res.cloudinary.com/yourcloud/image/fetch' -%}
{%- assign original_url = product.featured_image | image_url: width: 2000 | prepend: 'https:' -%}

<img
  src="{{ cloudinary_base }}/w_800,f_auto,q_auto/{{ original_url }}"
  alt="{{ product.featured_image.alt | escape }}"
  loading="lazy"
>

Image Upload Optimization

Image TypeDimensionsFormatNotes
Product2048×2048JPEG/PNGSquare, white background
Collection2000×1000JPEG2:1 aspect ratio
Hero2880×1000JPEGWide format
Logo400×200PNG/SVGTransparent background

Pre-Upload Optimization

Before uploading to Shopify:

# Optimize product images
mogrify -resize 2048x2048 -quality 85 -strip *.jpg

# Convert to sRGB
mogrify -colorspace sRGB -profile sRGB.icc *.jpg

App Integration

Using Apps for Optimization

Popular optimization apps:

  1. Crush.pics: Automatic compression
  2. TinyIMG: Compression + SEO
  3. Avada SEO: Image alt text automation

Custom App Integration

{%- comment -%} Check for optimized version from app {%- endcomment -%}
{%- if product.metafields.optimization_app.optimized_image -%}
  {{ product.metafields.optimization_app.optimized_image | image_url: width: 800 | image_tag }}
{%- else -%}
  {{ product.featured_image | image_url: width: 800 | image_tag }}
{%- endif -%}

Debugging and Testing

Check Image Sizes

{%- comment -%} Debug output {%- endcomment -%}
{% if request.design_mode %}
  <div class="debug-info">
    <p>Image: {{ image.src }}</p>
    <p>Original: {{ image.width }}×{{ image.height }}</p>
    <p>Aspect Ratio: {{ image.aspect_ratio }}</p>
  </div>
{% endif %}

Performance Testing

Use these tools:

  1. Chrome DevTools Network tab
  2. Lighthouse audits
  3. WebPageTest with Shopify stores
  4. Shopify’s built-in speed report

Summary

Quick Reference

TaskLiquid Code
Basic resize{{ image | image_url: width: 800 }}
With crop{{ image | image_url: width: 400, height: 400, crop: 'center' }}
Full img tag{{ image | image_url: width: 800 | image_tag }}
Lazy loading{{ image | image_url: width: 800 | image_tag: loading: 'lazy' }}
Alt text{{ image | image_url: width: 800 | image_tag: alt: product.title }}

Optimization Checklist

  1. ✅ Use image_url filter for all images
  2. ✅ Implement responsive srcset for product/collection images
  3. ✅ Eager load above-fold images (section.index == 1)
  4. ✅ Lazy load below-fold images
  5. ✅ Add fetchpriority=“high” to LCP images
  6. ✅ Include width/height attributes for CLS prevention
  7. ✅ Use appropriate sizes attribute
  8. ✅ Preload hero images in template head
  9. ✅ Upload high-quality source images (2048px+)
  10. ✅ Optimize images before upload (strip metadata)
  11. ✅ Use meaningful alt text for SEO
  12. ✅ Test with Lighthouse and speed reports
  13. ✅ Consider art direction with picture element
  14. ✅ Implement placeholder loading states

Shopify’s built-in image optimization handles the heavy lifting, but proper implementation of Liquid filters and loading strategies ensures your store achieves optimal performance.

Ready to optimize your images?

Sirv automatically optimizes, resizes, and converts your images. Try it free.

Start Free Trial