---
title: 'Flatten nested render calls, especially inside loops'
description: >-
  Flatten or inline nested snippets to reduce the compounding execution overhead
  of render tags, especially inside loops, and replace every deprecated include
  tag with render.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-nested-renders
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-nested-renders.md
api_name: liquid
---

# Flatten nested render calls, especially inside loops

Flatten or inline nested snippets to reduce the compounding execution overhead of [`render`](https://shopify.dev/docs/api/liquid/tags/render) tags, especially inside loops.

**Don't use \`include\`:**

[`include`](https://shopify.dev/docs/api/liquid/tags/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.

***

## Why

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`](https://shopify.dev/docs/api/liquid/tags/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.

***

## How

### Identify nesting depth

Use the [Theme Inspector](https://shopify.dev/docs/storefronts/themes/tools/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.

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

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

### Use 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](https://shopify.dev/docs/storefronts/themes/best-practices/performance/reduce-stylesheet-count) for the full pattern.

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

***

## Examples

### Before: three render calls per product

## sections/collection.liquid

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

## snippets/product-card.liquid

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

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

### After: flattened to one level

## snippets/product-card.liquid

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

***

## Testing

* Use the [Theme Inspector](https://shopify.dev/docs/storefronts/themes/tools/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](https://developer.chrome.com/docs/devtools/network) 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.

***

## References

* [`render`](https://shopify.dev/docs/api/liquid/tags/render) tag
* [`include`](https://shopify.dev/docs/api/liquid/tags/include) tag (deprecated, don't use)
* [`for`](https://shopify.dev/docs/api/liquid/tags/for) tag
* [`collection.products`](https://shopify.dev/docs/api/liquid/objects/collection#collection-products)
* [Reduce stylesheet count](https://shopify.dev/docs/storefronts/themes/best-practices/performance/reduce-stylesheet-count)
* [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)
* [Move operations outside loops](https://shopify.dev/docs/storefronts/themes/best-practices/performance/move-operations-outside-loops)

***
