Skip to main content

Filter collections before entering loops

Pre-filter collections using Liquid filters before loops. Move complex conditional logic outside of for loops to reduce processing overhead that multiplies across iterations.


Conditional checks have a processing cost. When placed inside a for loop, the number of iterations multiplies this cost. A collection with 50 products and 3 conditional checks per iteration means 150 conditional evaluations. If you can pre-filter the collection down to 12 matching products before entering the loop, then you evaluate 12 iterations with 1 conditional each: 12 evaluations instead of 150.

Complex conditions with multiple and/or operators are more expensive because each operator requires a separate evaluation. This processing adds directly to server-side Liquid rendering time, increasing TTFB, which in turn delays FCP and LCP.


Use Liquid filters to narrow the collection before the loop starts:

  • where: Filters objects by a property value. collection.products | where: "available" returns only available products.
  • map: Extracts a single property from each object. Useful for building arrays to check against.
  • sort: Sorts objects by a property. Sort once before the loop rather than sorting within the loop.
  • compact: Removes nil values from an array after mapping or filtering.
  • uniq: Removes duplicates from an array.

You can chain multiple filters together to apply several conditions before the loop:

{% assign filtered = collection.products | where: "available" | sort: "price" %}

For conditions that can't be expressed as where filters, such as price ranges, tag checks, and compound logic, pre-compute a lookup variable before the loop to simplify the in-loop check.


Less performant:

{%- for product in collection.products -%}
{% if product.available %}
{{ product.title }} - {{ product.price | money }}
{% endif %}
{%- endfor -%}

More performant:

{% assign available_products = collection.products | where: "available" %}

{%- for product in available_products -%}
{{ product.title }} - {{ product.price | money }}
{%- endfor -%}

Anchor to Compound filtering: available, tagged, and within a price rangeCompound filtering: available, tagged, and within a price range

When you need multiple conditions, apply as many as possible before the loop using filters, then handle the remaining conditions with a single in-loop check:

{%- comment -%}
Pre-filter: available products only.
The where filter handles the boolean check outside the loop.
{%- endcomment -%}
{% assign candidates = collection.products | where: "available" %}

{%- comment -%}
In-loop: check tag and price range.
These conditions can't be expressed with the where filter,
but the loop now iterates over fewer products.
{%- endcomment -%}
{%- for product in candidates -%}
{% if product.tags contains 'featured' and product.price >= 1000 and product.price <= 5000 %}
<div class="product-card">
{{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</div>
{% endif %}
{%- endfor -%}

Anchor to Pre-compute a lookup variablePre-compute a lookup variable

When you need to check a product against a list of values, build the list before the loop so the lookup is a simple contains check:

{%- comment -%}
Build a comma-separated list of promoted product handles
from a metafield, then check membership inside the loop.
{%- endcomment -%}
{% assign promoted_handles = section.settings.promoted_products | map: "handle" %}

{%- for product in collection.products -%}
{% if promoted_handles contains product.handle %}
<div class="product-card product-card--promoted">
{{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
<h3>{{ product.title }}</h3>
</div>
{% else %}
<div class="product-card">
{{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
<h3>{{ product.title }}</h3>
</div>
{% endif %}
{%- endfor -%}

Anchor to Chaining multiple filtersChaining multiple filters

Combine where, sort, and other filters to build the exact dataset before iterating:

{%- comment -%}
Get available products, sort by price ascending.
The loop body has no conditionals at all.
{%- endcomment -%}
{% assign sorted_available = collection.products | where: "available" | sort: "price" %}

{%- for product in sorted_available -%}
<div class="product-card">
{{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</div>
{%- endfor -%}

  • Use the Theme Inspector Chrome extension to identify expensive sections. Look for sections with high Liquid rendering times that contain loops.
  • Compare TTFB in the Chrome DevTools Network panel before and after pre-filtering. Filter to the document request and check the Waiting for server response time.
  • Focus on pages with the largest collections: collection pages with 50 or more products, search results, and pages with many section blocks.


Was this page helpful?