---
title: Move repeated assign and filter calls outside loops
description: >-
  Calculate values that don't depend on the loop variable once before the loop
  starts and reuse the result, rather than recalculating on every iteration.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/move-operations-outside-loops
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/move-operations-outside-loops.md
api_name: liquid
---

# Move repeated assign and filter calls outside loops

Move calculations and filter operations outside of loops. Assign the result to a variable before the loop and reuse it, rather than recalculating on each iteration.

***

## Why

When you perform an operation inside a [`for`](https://shopify.dev/docs/api/liquid/tags/for) loop that doesn't depend on the loop variable, you're calculating the same thing multiple times. The result doesn't change between iterations, but Liquid executes the operation again each time. Moving these operations before the loop reduces server-side Liquid execution time.

The impact scales with both the number of iterations and the cost of the operation. A `sort` filter inside a 50-product loop runs 50 times, producing the same sorted array each time. An `asset_url` filter that resolves the same asset path on every iteration wastes time resolving a value that could be resolved once. These add directly to TTFB, which delays FCP and LCP.

***

## How

Identify any operation inside a loop whose result doesn't depend on the current iteration's variable. Move it above the loop and assign it to a variable.

Common operations to hoist:

* **`assign` with static values**: Any `assign` that doesn't reference the loop variable.
* **`capture` blocks**: Blocks that produce the same output on every iteration.
* **`image_url`**: When the image doesn't change per iteration, for example, a placeholder or brand logo.
* **`asset_url`**: Asset paths are the same on every iteration. Resolve them once.
* **`sort` and `where` filters**: Sorting or filtering a collection produces the same result each time.
* **`money` filter on static values**: Formatting a fixed price or threshold.
* **`size`**: Counting the size of an array that doesn't change inside the loop.

***

## Examples

### Basic: sort operation inside a loop

```liquid
{%- comment -%} Less performant: sorts the entire collection on every iteration {%- endcomment -%}
{%- for product in collection.products -%}
  {% assign sorted = collection.products | sort: "price" %}
  {{ product.title }}
{%- endfor -%}


{%- comment -%} More performant: sort once before the loop {%- endcomment -%}
{% assign sorted = collection.products | sort: "price" %}


{%- for product in sorted -%}
  {{ product.title }}
{%- endfor -%}
```

### Real-world product card snippet with multiple hoistable operations

This example shows a product card loop where several operations are repeated on every iteration but produce the same result:

Less performant:

```liquid
{%- for product in collection.products -%}
  {% assign placeholder = 'product-1' | placeholder_svg_tag %}
  {% assign badge_icon = 'icon-badge.svg' | asset_url %}
  {% assign free_shipping_threshold = section.settings.free_shipping_threshold | money %}
  {% assign total_products = collection.products | size %}


  <div class="product-card">
    {% if product.featured_image != blank %}
      {{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
    {% else %}
      {{ placeholder }}
    {% endif %}


    <h3>{{ product.title }}</h3>
    <p>{{ product.price | money }}</p>


    {% if product.price >= section.settings.free_shipping_threshold %}
      <img src="{{ badge_icon }}" alt="" width="16" height="16">
      <span>Free shipping on orders over {{ free_shipping_threshold }}</span>
    {% endif %}


    <p>{{ forloop.index }} of {{ total_products }}</p>
  </div>
{%- endfor -%}
```

More performant:

```liquid
{%- comment -%}
  These values are the same on every iteration.
  Calculate them once before the loop.
{%- endcomment -%}
{% assign placeholder = 'product-1' | placeholder_svg_tag %}
{% assign badge_icon = 'icon-badge.svg' | asset_url %}
{% assign free_shipping_threshold_formatted = section.settings.free_shipping_threshold | money %}
{% assign total_products = collection.products | size %}


{%- for product in collection.products -%}
  <div class="product-card">
    {% if product.featured_image != blank %}
      {{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
    {% else %}
      {{ placeholder }}
    {% endif %}


    <h3>{{ product.title }}</h3>
    <p>{{ product.price | money }}</p>


    {% if product.price >= section.settings.free_shipping_threshold %}
      <img src="{{ badge_icon }}" alt="" width="16" height="16">
      <span>Free shipping on orders over {{ free_shipping_threshold_formatted }}</span>
    {% endif %}


    <p>{{ forloop.index }} of {{ total_products }}</p>
  </div>
{%- endfor -%}
```

In this example, four operations moved outside the loop. With 50 products, that's 200 fewer filter evaluations (4 operations × 50 iterations).

### Hoisting capture blocks

```liquid
{%- comment -%} Less performant: capture rebuilds the same string each iteration {%- endcomment -%}
{%- for product in collection.products -%}
  {% capture empty_state %}
    <div class="empty-state">
      <p>{{ 'products.no_image' | t }}</p>
    </div>
  {% endcapture %}


  {% if product.featured_image != blank %}
    {{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
  {% else %}
    {{ empty_state }}
  {% endif %}
{%- endfor -%}


{%- comment -%} More performant: capture once before the loop {%- endcomment -%}
{% capture empty_state %}
  <div class="empty-state">
    <p>{{ 'products.no_image' | t }}</p>
  </div>
{% endcapture %}


{%- for product in collection.products -%}
  {% if product.featured_image != blank %}
    {{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
  {% else %}
    {{ empty_state }}
  {% endif %}
{%- endfor -%}
```

***

## Testing

* Use the [Theme Inspector](https://shopify.dev/docs/storefronts/themes/tools/theme-inspector) Chrome extension to identify repeated operations inside loops. The flame graph shows per-snippet timing, and the sandwich view shows cumulative time spent on repeated operations.
* Compare TTFB in the [Chrome DevTools Network panel](https://developer.chrome.com/docs/devtools/network) before and after hoisting operations. Filter to the document request and check the **Waiting for server response** time.
* Focus on collection pages and other pages where loops iterate many times. The impact scales with the iteration count.

***

## References

* [`for`](https://shopify.dev/docs/api/liquid/tags/for) tag
* [`assign`](https://shopify.dev/docs/api/liquid/tags/assign) tag
* [`capture`](https://shopify.dev/docs/api/liquid/tags/capture) tag
* [`asset_url`](https://shopify.dev/docs/api/liquid/filters/asset_url) filter
* [`image_url`](https://shopify.dev/docs/api/liquid/filters/image_url) filter
* [`money`](https://shopify.dev/docs/api/liquid/filters/money) filter
* [`collection.products`](https://shopify.dev/docs/api/liquid/objects/collection#collection-products)
* [Debugging common causes for slow loading in Shopify Liquid storefronts](https://performance.shopify.com/blogs/blog/debugging-common-causes-for-slow-loading-in-shopify-liquid-storefronts)
* [Avoid deeply nested Liquid loops](https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-deeply-nested-liquid-loops)
* [Avoid excessive conditionals in loops](https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-conditionals-in-loops)
* [Avoid repetitive filter calls](https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-repetitive-filter-calls)

***
