---
title: Output strings with echo instead of append and prepend
description: >-
  Output strings directly using echo or output tags instead of filter-based
  concatenation with append or prepend to reduce per-call overhead, especially
  inside loops.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-echo-for-concatenation
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-echo-for-concatenation.md
api_name: liquid
---

# Output strings with echo instead of append and prepend

Use the [`echo`](https://shopify.dev/docs/api/liquid/tags/echo) tag or `{{ }}` output tags for string concatenation instead of filters like [`append`](https://shopify.dev/docs/api/liquid/filters/append) or [`prepend`](https://shopify.dev/docs/api/liquid/filters/prepend).

***

## Why

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.

***

## How

Use direct output instead of filter-based concatenation:

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

### Use `echo` inside `{% liquid %}` blocks

The [`echo`](https://shopify.dev/docs/api/liquid/tags/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 %}`](https://shopify.dev/docs/api/liquid/tags/liquid) block, where `{{ }}` isn't available:

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

***

## Examples

### Building CSS classes

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

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

### Constructing URLs

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

### Assembling 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:

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

### When 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:

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

***

## Testing

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

***

## References

* [`echo`](https://shopify.dev/docs/api/liquid/tags/echo) tag
* [`append`](https://shopify.dev/docs/api/liquid/filters/append) filter
* [`prepend`](https://shopify.dev/docs/api/liquid/filters/prepend) filter
* [`assign`](https://shopify.dev/docs/api/liquid/tags/assign) tag
* [`liquid`](https://shopify.dev/docs/api/liquid/tags/liquid) tag
* [`divided_by`](https://shopify.dev/docs/api/liquid/filters/divided_by) filter
* [`capture`](https://shopify.dev/docs/api/liquid/tags/capture) tag
* [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)

***
