Skip to main content

Use Liquid filter chains instead of manual URL construction

Generate image markup through Shopify's optimized filter system instead of manually constructing URLs and attributes, to ensure automatic access to platform performance optimizations.


Manual URL construction bypasses Shopify's automatic optimizations: responsive srcset generation, width and height attributes, focal point positioning, format negotiation for WebP and AVIF, CDN parameters, and Early Hints support.

Filter chains are also faster to render than the equivalent hand-written Liquid, because the filters are implemented natively instead of being interpreted as template code. How much faster depends on the page: the gap is small on a page with a few images and large on a page that builds many URLs, such as a variant-heavy product page. Measure your own pages with Theme Inspector rather than assuming a fixed number.

Native filters are compiled to bytecode for faster execution, use optimized algorithms for efficient URL generation, cache aspect ratio and dimension calculations, and provide direct CDN integration with optimal URL parameters. Manual string manipulation runs interpreted Liquid code, which is significantly slower.

Filter chains are less error-prone and automatically benefit from platform improvements without code changes.


Anchor to The filter chain patternThe filter chain pattern

Pipe image objects through image_url then image_tag:

{{ product.featured_image
| image_url: width: 800
| image_tag:
widths: '400, 600, 800',
loading: 'lazy'
}}

Because image_url requests a width of 800, candidate widths above 800 are dropped, and the rendered srcset contains the 400w, 600w, and 800w entries.

This generates complete optimized markup with srcset, dimensions, focal points, and proper CDN URLs.

Anchor to Automatic optimizationsAutomatic optimizations

  • Responsive images: Automatically generates a srcset. The candidate widths start from the defaults 352, 832, 1200, and 1920. If you pass width to image_url, then that width is added to the set and the set is re-sorted. If you pass widths to image_tag, then your list replaces the defaults entirely. The set is then filtered to widths no larger than the width of the image_url output, so the markup never asks for an upscaled image. If nothing survives that filter, then no srcset attribute is emitted at all.
  • width and height attributes: Automatically generates correct dimensions based on aspect ratio, preventing CLS by reserving space before the image loads.
  • Focal point positioning: Automatically applies an object-position style if a focal point is set on the image.
  • Format negotiation: The CDN automatically serves WebP or AVIF when the browser supports them, reducing file size by 25 to 35 percent.
  • Cache optimization: Includes a version parameter for proper cache invalidation.
  • Early Hints support: Use the preload: true parameter to trigger HTTP 103 Early Hints, so the image can start downloading before HTML parsing.

Anchor to Migrate from manual constructionMigrate from manual construction

Identify manual construction patterns by searching your theme for:

  • | replace: in image contexts.
  • Direct image.src usage in <img> tags.
  • Manual srcset construction.
  • Hardcoded width and height on images.

Replace these patterns:

{%- comment -%} Anti-pattern: manual construction {%- endcomment -%}
<img src="{{ product.featured_image.src | replace: '.jpg', '_x800.jpg' }}" />

{%- comment -%} Recommended: filter chain {%- endcomment -%}
{{ product.featured_image | image_url: width: 800 | image_tag }}

Then enhance with performance parameters like widths, sizes, and loading.

Test to verify that images display correctly, check responsive behavior at different viewports, and measure performance improvement with Theme Inspector.

For CSS background images, use filter for URL only:

<div
class="hero-banner"
style="background-image: url({{ section.settings.image | image_url: width: 2000 }});"
>
</div>
Note

<img> tags with object-fit: cover are more performant than background images because they support native lazy loading, responsive images through srcset, automatic dimensions (which prevents CLS), and fetch priority hints.


Anchor to Manual construction anti-patternsManual construction anti-patterns

Common mistakes when building image URLs manually:

{%- comment -%} Mistake 1: string replacement for sizing {%- endcomment -%}
<img src="{{ product.featured_image.src | replace: '.jpg', '_800x.jpg' }}" />

{%- comment -%} Mistake 2: manual srcset construction {%- endcomment -%}
<img
src="{{ product.featured_image.src }}"
srcset="{{ product.featured_image.src | replace: '.jpg', '_400x.jpg' }} 400w,
{{ product.featured_image.src | replace: '.jpg', '_800x.jpg' }} 800w"
/>

{%- comment -%} Mistake 3: using image.src directly {%- endcomment -%}
<img src="{{ product.featured_image.src }}" alt="{{ product.title }}" />

What's missing:

  • No automatic srcset.
  • No correct width and height based on aspect ratio.
  • No focal point positioning.
  • No format negotiation.
  • No CDN optimization parameters.
  • No cache versioning.
  • No Early Hints support.
  • Breaks when Shopify updates the CDN URL structure.
  • Doesn't benefit from the native filter implementations, so it renders more slowly.

{{ product.featured_image
| image_url: width: 800
| image_tag:
widths: '400, 600, 800',
sizes: '(min-width: 750px) 50vw, 100vw',
loading: 'lazy',
alt: product.title
}}

Anchor to Hero image with all optimizationsHero image with all optimizations

{%- comment -%}
Test the below-the-fold case positively so a nil section.index, which happens in the
theme editor, in static sections, and in Section Rendering API responses, still renders
the hero.
{%- endcomment -%}
{% unless section.index > 1 %}
{{ section.settings.image
| image_url: width: 2400
| image_tag:
widths: '800, 1200, 1600, 2000, 2400',
sizes: '100vw',
loading: 'eager',
fetchpriority: 'high',
preload: true,
class: 'hero-image',
alt: section.settings.heading
}}
{% endunless %}

A single filter chain provides:

  • Responsive images.
  • Correct dimensions.
  • Focal points.
  • Format optimization.
  • Early Hints.
  • Future compatibility.

A manual implementation would require more than 20 lines of complex Liquid and would miss platform optimizations.

{% paginate collection.products by 12 %}
{%- for product in collection.products -%}
<div class="product-card">
{%- if forloop.index <= 4 -%}
{%- assign card_loading = 'eager' -%}
{%- else -%}
{%- assign card_loading = 'lazy' -%}
{%- endif -%}
{{ product.featured_image
| image_url: width: 600
| image_tag:
widths: '300, 400, 500, 600',
sizes: '(min-width: 990px) 25vw, (min-width: 750px) 33vw, 50vw',
loading: card_loading
}}
</div>
{%- endfor -%}
{% endpaginate -%}

Automatic benefits:

  • Correct width and height for each image.
  • Responsive srcset for all viewports.
  • The first 4 images load eagerly and the rest load lazily.
  • Focal points are applied if set.
  • WebP or AVIF is served when supported.

The {% paginate %} tag limits the query to 12 products, not the filter chain. Inside a paginate block the page size already bounds the loop, so a limit on the for tag is redundant.

Before (10 lines, no optimizations, complex):

{%- assign img_url = product.featured_image.src -%}
<img
src="{{ img_url }}"
srcset="
{{ img_url | replace: '.jpg', '_400x.jpg' }} 400w,
{{ img_url | replace: '.jpg', '_800x.jpg' }} 800w,
{{ img_url | replace: '.jpg', '_1200x.jpg' }} 1200w
"
alt="{{ product.title }}"
/>

After (4 lines, all automatic optimizations, simple):

{{ product.featured_image
| image_url: width: 1200
| image_tag: widths: '400, 800, 1200'
}}

Anchor to Collection images with croppingCollection images with cropping

{{ collection.image
| image_url: width: 600, height: 400, crop: 'center'
| image_tag:
widths: '300, 400, 500, 600',
sizes: '(min-width: 750px) 300px, 50vw'
}}

Cropping is handled in the image_url filter, and responsive handling is in the image_tag filter.


  • Use Theme Inspector to compare Liquid render time between manual construction and filter chains.
  • Check the Chrome DevTools Network panel to verify that the srcset generates correct image URLs.
  • Use the Chrome DevTools Network panel to check for format negotiation (WebP or AVIF delivery). Look at the Content-Type response header.
  • Visually inspect to confirm that focal points apply correctly.
  • Measure performance improvement before and after migration.


Was this page helpful?