Skip to main content

Flatten nested render calls, especially inside loops

Flatten or inline nested snippets to reduce the compounding execution overhead of render tags, especially inside loops.

Don't use include

include is deprecated, and you shouldn't use it anywhere in a theme. Unlike render, include gives the snippet access to the entire parent scope and lets it modify the caller's variables. That lack of scope isolation prevents the Liquid renderer from applying optimizations that it applies to render, so an include is slower than the equivalent render before nesting multiplies the cost at all. Replace every include with render, and pass the variables that the snippet needs as explicit arguments.


Each {% render %} tag has a fixed cost: Liquid must look up the snippet file, create a new scope, pass variables into that scope, execute the snippet, and return the result. This cost is small for a single render, but it compounds when snippets are nested. A snippet that renders another snippet that renders a third creates three layers of overhead per call.

Inside a for loop, this multiplication becomes significant. If a product card snippet renders a price snippet and a badge snippet, and you have 50 products in a collection, then that's 50 × 3 = 150 render calls. Flattening the nesting to a single level reduces it to 50 render calls. Inlining the simplest snippets can reduce it further.

This overhead adds directly to server-side Liquid rendering time, increasing Time to First Byte (TTFB), which in turn delays FCP and LCP.


Anchor to Identify nesting depthIdentify nesting depth

Use the Theme Inspector Chrome extension to visualize snippet nesting. In the flame graph, look for deeply nested patterns where one snippet calls another in a chain. Nesting three or more levels deep is a sign of opportunity.

Anchor to Inline simple markupInline simple markup

If a snippet contains only a few lines of HTML with no conditional logic, then inline it directly into the parent. Reserve {% render %} for snippets that contain substantial logic or are reused across many sections.

Anchor to Flatten nested snippetsFlatten nested snippets

When a snippet renders other snippets, consider merging them into a single snippet. A product card that calls render 'product-price' and render 'product-badge' separately can often combine all three into one file.

Anchor to Use the ,[object Object], pattern to avoid duplicate resource loadingUse the skip_styles pattern to avoid duplicate resource loading

When keeping a snippet as a render, such as a product card used in multiple sections, add a skip_styles parameter to prevent loading the same stylesheet on every iteration. See Reduce stylesheet count for the full pattern.

{%- assign skip_card_product_styles = false -%}
{%- for product in collection.products -%}
{% render 'card-product',
product: product,
skip_styles: skip_card_product_styles
%}
{%- assign skip_card_product_styles = true -%}
{%- endfor -%}
Render arguments can't contain comparisons

Liquid parses render arguments as name-value pairs, so a comparison like skip_styles: forloop.index > 1 binds skip_styles to forloop.index and silently drops the > 1. The snippet receives a number, and every number is truthy in Liquid, so {% unless skip_styles %} never runs and the stylesheet never loads. Assign the comparison to a variable first, then pass the variable.


Anchor to Before: three render calls per productBefore: three render calls per product

sections/collection.liquid

{%- for product in collection.products -%}
{% render 'product-card', product: product %}
{%- endfor -%}

snippets/product-card.liquid

<div class="product-card">
{{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
<h3>{{ product.title }}</h3>
{% render 'product-price', product: product %}
{% render 'product-badge', product: product %}
</div>

snippets/product-price.liquid

<div class="price">
{% if product.compare_at_price > product.price %}
<span class="price--sale">{{ product.price | money }}</span>
<s>{{ product.compare_at_price | money }}</s>
{% else %}
<span>{{ product.price | money }}</span>
{% endif %}
</div>

With 50 products: 50 renders of product-card, each triggering 2 more renders (product-price and product-badge), for 150 total render calls.

Anchor to After: flattened to one levelAfter: flattened to one level

snippets/product-card.liquid

{%- unless skip_styles -%}
{{ 'component-card.css' | asset_url | stylesheet_tag }}
{%- endunless -%}

<div class="product-card">
{{ product.featured_image | image_url: width: 400 | image_tag: loading: 'lazy' }}
<h3>{{ product.title }}</h3>

<div class="price">
{% if product.compare_at_price > product.price %}
<span class="price--sale">{{ product.price | money }}</span>
<s>{{ product.compare_at_price | money }}</s>
{% else %}
<span>{{ product.price | money }}</span>
{% endif %}
</div>

{% if product.available == false %}
<span class="badge badge--sold-out">Sold out</span>
{% elsif product.compare_at_price > product.price %}
<span class="badge badge--sale">Sale</span>
{% endif %}
</div>

With 50 products: 50 total render calls instead of 150. The price and badge markup is inlined because it's simple HTML with basic conditionals.

Note

For complex snippets with substantial logic, such as variant pickers, media galleries, and structured data markup, keeping them as separate renders is reasonable. The goal is to avoid nesting simple markup that doesn't benefit from the abstraction.


  • Use the Theme Inspector Chrome extension to visualize nesting depth. Look for deeply nested flame graph patterns on collection pages.
  • Compare TTFB in the Chrome DevTools Network panel before and after flattening renders. Filter to the document request and check the Waiting for server response (TTFB) value.
  • Focus on collection pages and other pages with loops that iterate many times, such as product grids, blog post lists, and navigation menus with many items.


Was this page helpful?