Skip to main content

Output strings with echo instead of append and prepend

Use the echo tag or {{ }} output tags for string concatenation instead of filters like append or prepend.


Each Liquid filter adds a small amount of processing overhead. For simple string concatenation, direct output avoids this overhead entirely and reduces server-side Liquid execution time.

This is a micro-optimization. In isolation, the difference per call is minimal. In complex themes, these calls accumulate. A product card snippet that uses append five times, rendered 50 times in a collection loop, means 250 filter calls that could be zero. The overhead compounds in the same way that other loop optimizations do: small per-iteration savings multiplied across many iterations add up to measurable TTFB improvements.


Use direct output instead of filter-based concatenation:

{%- comment -%} Less performant: uses append filter {%- endcomment -%}
{%- assign greeting = "Hello " | append: customer.first_name -%}
{{ greeting }}

{%- comment -%} More performant: direct output {%- endcomment -%}
Hello {{ customer.first_name }}

When you need the result stored in a variable for later use, assign with append is appropriate. The optimization applies when you're building a string only to output it immediately.

The echo tag outputs a value the same way {{ }} does. It works anywhere in a template, and it's the only way to output a value inside a {% liquid %} block, where {{ }} isn't available:

{%- comment -%} Less performant: build the string, then output it {%- endcomment -%}
{%- liquid
assign heading = product.vendor | append: " - " | append: product.title
echo heading
-%}

{%- comment -%} More performant: echo each part {%- endcomment -%}
{%- liquid
echo product.vendor
echo " - "
echo product.title
-%}

Anchor to Building CSS classesBuilding CSS classes

A common pattern in theme snippets is building a CSS class string:

{%- comment -%} Less performant: filter chain to build class string {%- endcomment -%}
{%- assign card_classes = "product-card" -%}
{%- if product.available == false -%}
{%- assign card_classes = card_classes | append: " product-card--sold-out" -%}
{%- endif -%}
{%- if product.compare_at_price > product.price -%}
{%- assign card_classes = card_classes | append: " product-card--on-sale" -%}
{%- endif -%}
<div class="{{ card_classes }}">

{%- comment -%} More performant: direct output with capture {%- endcomment -%}
<div class="product-card
{%- if product.available == false %} product-card--sold-out{% endif -%}
{%- if product.compare_at_price > product.price %} product-card--on-sale{% endif -%}
">

{%- comment -%} Less performant: append filters for URL {%- endcomment -%}
{%- assign product_url = shop.url | append: "/products/" | append: product.handle -%}
<a href="{{ product_url }}">{{ product.title }}</a>

{%- comment -%} More performant: direct output {%- endcomment -%}
<a href="{{ shop.url }}/products/{{ product.handle }}">{{ product.title }}</a>

Anchor to Assembling structured data inside a loopAssembling structured data inside a loop

When building structured data (JSON-LD) for product cards in a collection loop, direct output avoids filter overhead on every iteration.

Be careful with filter order when you build JSON with append. In '"price": "' | append: product.price | money_without_currency, the money filter applies to the whole concatenated string, not to the price: the string parses as 0.0 and the block silently renders 0.00.

Don't use money_without_currency for a schema.org price either. It formats using the store's currency settings, so locales with a comma decimal separator produce an invalid price value. Output a plain decimal instead:

{%- comment -%} Less performant: building JSON strings with append {%- endcomment -%}
{%- for product in collection.products -%}
{%- assign json_name = '"name": "' | append: product.title | append: '"' -%}
{%- assign formatted_price = product.price | divided_by: 100.0 -%}
{%- assign json_price = '"price": "' | append: formatted_price | append: '"' -%}
<script type="application/ld+json">{"@type": "Product", {{ json_name }}, {{ json_price }}}</script>
{%- endfor -%}

{%- comment -%} More performant: direct output, no assign or append needed {%- endcomment -%}
{%- for product in collection.products -%}
<script type="application/ld+json">
{
"@type": "Product",
"name": {{ product.title | json }},
"price": "{{ product.price | divided_by: 100.0 }}"
}
</script>
{%- endfor -%}

Anchor to When filters are still the right choiceWhen filters are still the right choice

Filters are appropriate when you need the result in a variable for reuse, or when the transformation is complex:

{%- comment -%}
This assign and append combination is fine, because the variable is used in multiple places.
{%- endcomment -%}
{%- assign full_name = customer.first_name | append: " " | append: customer.last_name -%}
<h1>Welcome, {{ full_name }}</h1>
<meta name="author" content="{{ full_name }}">

  • Use the Theme Inspector Chrome extension to measure the impact on Liquid rendering time. Look at per-snippet timing in the flame graph.
  • Compare TTFB in the Chrome DevTools Network panel before and after applying this pattern. Filter to the document request and check Waiting for server response.
  • The impact is most visible on collection pages and other pages where product card snippets render many times.


Was this page helpful?