---
title: Use Liquid filter chains instead of manual URL construction
description: >-
  Generate image markup through Shopify's optimized `image_url` and `image_tag`
  filter chain to automatically get responsive srcset, correct dimensions, focal
  points, format negotiation, and CDN optimization.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-filter-chains
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-filter-chains.md
api_name: liquid
---

# 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.

***

## Why

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.

***

## How

### The filter chain pattern

Pipe image objects through `image_url` then `image_tag`:

```liquid
{{ 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.

### Automatic 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.

### Migrate 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:

```liquid
{%- 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.

### Background images

For CSS background images, use filter for URL only:

```liquid
<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.

***

## Examples

### Manual construction anti-patterns

Common mistakes when building image URLs manually:

```liquid
{%- 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.

### Recommended filter chain pattern

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

### Hero image with all optimizations

```liquid
{%- 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.

### Product grid

```liquid
{% 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.

### Replace manual `srcset` construction

Before (10 lines, no optimizations, complex):

```liquid
{%- 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):

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

### Collection images with cropping

```liquid
{{ 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.

***

## Testing

* Use Theme Inspector to compare Liquid render time between manual construction and filter chains.
* Check the [Chrome DevTools Network panel](https://developer.chrome.com/docs/devtools/network) to verify that the `srcset` generates correct image URLs.
* Use the [Chrome DevTools Network panel](https://developer.chrome.com/docs/devtools/network) 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.

***

## References

* [`image_tag`](https://shopify.dev/docs/api/liquid/filters/image_tag) filter
* [`image_url`](https://shopify.dev/docs/api/liquid/filters/image_url) filter
* [`product.featured_image`](https://shopify.dev/docs/api/liquid/objects/product#product-featured_image)
* [`focal_point`](https://shopify.dev/docs/api/liquid/objects/focal_point) object
* [Shopify CDN](https://shopify.dev/docs/storefronts/themes/best-practices/performance/platform#shopify-cdn)
* [Responsive images on Shopify with Liquid](https://performance.shopify.com/blogs/blog/responsive-images-on-shopify-with-liquid)
* [Optimizing images for performance on Shopify](https://performance.shopify.com/blogs/blog/optimizing-images-for-performance-on-shopify)

***
