> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Why render Is Faster Than include, and Its Real Cost
- URL: https://debugly.dev/shopify-liquid-render-vs-include-performance/
- Published: 2026-10-09T13:04:00.000Z
- Updated: 2026-10-23T09:13:04.000Z
- Description: Shopify deprecated include for a reason that is about scope, not syntax. The performance difference is real and the migration breaks things in a predictable…
- Author: Rohit Bhadani
- Tags: Shopify, Deep Dive, Performance, Web Development

`include` has been deprecated since 2019 and there are still production themes full of it. The usual reason given for migrating is "Shopify says so", which is not an argument that motivates anybody to touch a working theme.

The real reason is scope, and scope is why one of them can be optimised and the other cannot.

## What the difference actually is

`include` gives the snippet access to the **entire parent scope**. Every variable defined anywhere before the call is visible inside, and any assignment the snippet makes leaks back out.

`render` gives the snippet an **isolated scope**. It sees only what you explicitly pass, plus global objects like `shop`, `cart` and `settings`. Nothing it assigns escapes.

```liquid
{%- comment -%} include: shared scope, surprising both ways {%- endcomment -%}
{%- assign heading = "Featured" -%}
{% include 'product-card' %}
{{ heading }}
{%- comment -%} may no longer be "Featured": the snippet could have reassigned it {%- endcomment -%}

{%- comment -%} render: isolated, predictable {%- endcomment -%}
{% render 'product-card', product: product, heading: heading %}
{{ heading }}
{%- comment -%} still "Featured", guaranteed {%- endcomment -%}

```

Tested against the current Liquid runtime, Dawn 15.

## Why isolation makes it faster

Because the parser can know, before rendering, exactly what a `render` snippet depends on.

With `include`, the snippet's behaviour depends on the entire mutable state of the parent at the moment of the call. That state can differ on every iteration of a loop, so the output cannot be reused and the whole thing must be re-evaluated each time. The renderer also has to carry the full variable environment into every call.

With `render`, the inputs are declared. The same inputs produce the same output, which makes caching and parallel evaluation possible, and Shopify's renderer takes advantage of that.

There is a second, larger effect in loops. `render` supports the `for` form:

```liquid
{%- comment -%} one render call, iterating internally {%- endcomment -%}
{% render 'product-card' for collection.products as product %}

{%- comment -%} versus a call per iteration {%- endcomment -%}
{% for product in collection.products %}
  {% render 'product-card', product: product %}
{% endfor %}

```

The `for` form sets up the snippet once and iterates inside it, rather than re-entering the render machinery per item. On a 50 product collection page the difference is measurable in server response time, which is the metric Shopify's speed score weights most heavily and the one you have the least other leverage over.

## Measuring it rather than believing me

Liquid gives you a render time via `Server-Timing`, and Shopify exposes it on the response:

```bash
curl -sS -o /dev/null -D - "https://yourstore.com/collections/all" \
  | grep -i server-timing

```

```
server-timing: db;dur=48, parse;dur=12, render;dur=194, total;dur=261

```

`render` is your Liquid. That is the number this post is about, and it is the only part of the server response you directly control. Run it ten times and take the median, because a cold render is not representative.

For a controlled comparison, duplicate a template, convert one copy, and hit both:

```bash
for t in old new; do
  echo -n "$t: "
  for i in $(seq 10); do
    curl -sS -o /dev/null -D - "https://yourstore.com/collections/all?view=$t" \
      | grep -oi 'render;dur=[0-9.]*' | cut -d= -f2
  done | sort -n | awk '{a[NR]=$1} END{print a[int(NR/2)]}'
done

```

On the collection template I converted most recently, median render went from 194 ms to 141 ms. Not transformative, but it is 53 ms on every collection page view for a change with no visual output, and it compounds with everything else.

## How the migration breaks, predictably

Converting is mechanical until it is not. The failures cluster into four shapes.

**Silent nils from undeclared variables.** The snippet used `product` because the parent happened to have one in scope. Under `render` it is nil, and Liquid renders nil as an empty string rather than raising. Your product card simply goes blank, on some pages, with no error anywhere. This is the [Liquid fails silently](https://debugly.dev/shopify-liquid-error-silent-failure/) problem in its most annoying form.

**Loop variables.** `forloop` does not cross into a rendered snippet. If the snippet used `forloop.index`, pass it: `{% render 'card', product: product, index: forloop.index %}`.

**Assignments that were doing real work.** A snippet that computed a total and assigned it for the parent to print will now compute it into the void. Restructure so the parent computes and passes down, which is better code anyway.

**Recursive includes.** `include` allowed a snippet to include itself, and menus were built this way. `render` forbids it, and the replacement is the `for` form or an explicit depth limited structure. This is the one conversion that requires actual thought.

A grep that finds the risky ones before you start:

```bash
# every include, with the snippet name
grep -rn "{%[- ]*include" sections/ snippets/ templates/ layout/

# snippets that assign, which are the ones that may leak on purpose
grep -rln "{%[- ]*assign" snippets/

```

The intersection of those two lists is where your migration will hurt.

## A conversion order that does not break the store

**Start with leaf snippets.** Anything that only prints, assigns nothing, and is called from one place. These are safe and they build confidence.

**Convert one template at a time, not the whole theme.** Duplicate the template, convert the copy, compare rendered HTML byte for byte with `curl` and `diff`. Identical output means the conversion is correct.

```bash
curl -sS "https://yourstore.com/collections/all" > /tmp/old.html
curl -sS "https://yourstore.com/collections/all?view=new" > /tmp/new.html
diff <(sed 's/view=new//' /tmp/new.html) /tmp/old.html

```

**Do the product card last.** It is the most called snippet, it benefits most, and it has the most implicit dependencies. Do it when you know the theme.

**Keep the `for` form in mind.** Every `{% for %}` wrapping a single `{% render %}` is a free conversion to `{% render ... for ... %}`.

## Is it worth doing at all

If your theme renders in 60 ms, no. Go and look at your apps and your images instead, because that is where the time is.

If your collection templates are rendering in 200 ms or more and you have deep snippet nesting, then yes, and the scope isolation is worth more than the milliseconds. Debugging a theme where any snippet can silently mutate any variable is a genuinely worse experience, and I have lost hours to exactly that. The performance improvement is the excuse. Predictable scope is the reward.

---

Related reading: [How to Tell Which App Is Actually Slowing Your Store Down](https://debugly.dev/shopify-app-script-audit-third-party/) explores a connected angle.

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)