Shopify Essentials: Theme performance and Liquid optimisation

Liquid techniques that make a Shopify theme faster: shallow snippets, cheaper loops, lazy loading below the fold and scripts that load only where used.

Updated 6 min read

A Shopify theme’s speed starts with Liquid, the templating language that builds each page on Shopify’s servers. Deep snippet trees, repeated loops and array filters inside loops all add to the wait before the first byte reaches the browser.

This guide covers the Liquid fixes first, then the front-end loading that sits on top: images, scripts and CSS.

Why Liquid affects speed

Shopify renders Liquid on its servers. Each loop, filter and conditional adds time before the HTML is sent.

Deeply nested snippets and slow loops lead to:

  • A long Time to First Byte (TTFB)
  • Slowdowns under heavy traffic
  • Poor Core Web Vitals, Google’s page-speed measures

Measure before you change anything

Get a baseline first, so you know which change made the difference:

  • Shopify Theme Inspector for Chrome shows Liquid render time as a flame graph, traced to file and line
  • PageSpeed Insights gives lab scores and, for busier stores, real-user Core Web Vitals
  • WebPageTest shows TTFB and the full request waterfall

Test the templates that earn money: home, collection, product and cart.

Avoid deep snippet trees

Each render call is cheap on its own, but nesting multiplies it. A product grid that renders a card, which renders a price badge, which renders an icon, runs every one of those snippets once per product. Deep trees also make it hard to see where render time goes.

Inefficient nesting
{% render 'product-grid' %} → {% render 'product-card' %} → {% render 'price-badge' %}
Flatten when possible

Keep the per-item work in one snippet where you can, and pass it only the data it needs.

Reduce expensive loops

Nested or repeated loops are a common cause of slow rendering in custom themes.

Nested loops with filtering
{% for collection in collections %}
  {% for product in collection.products %}
    {% if product.available %} ... {% endif %}
  {% endfor %}
{% endfor %}
Optimised by pre-filtering

Loop over one collection you’ve already narrowed, and use limit so the loop stops early.

{% assign available_products = collections['frontpage'].products | where: 'available' %}
{% for product in available_products limit: 6 %}
  {{ product.title }}
{% endfor %}

Use section settings for control

Put what a merchant might want to change in the section’s schema settings, rather than hard-coding it:

{% if section.settings.enable_promo %}
  <div class="promo-banner">...</div>
{% endif %}

Do array work once, outside the loop

Simple string filters such as escape or upcase are cheap. Array filters such as where, sort and map work through a whole list, so they cost more when they repeat inside a loop.

The same sort, once per block
{% for block in section.blocks %}
  {% assign featured = collection.products | where: 'available' | sort: 'price' %}
  ...
{% endfor %}
Sort once, reuse it
{% assign featured = collection.products | where: 'available' | sort: 'price' %}
{% for block in section.blocks %}
  ...
{% endfor %}

Lazy-load below the fold, never the hero

Lazy loading helps images further down the page. On the hero image it does the opposite: the browser waits to fetch what is usually the Largest Contentful Paint (LCP) element, and LCP gets worse.

Shopify’s image_tag filter already sets loading="lazy" on images in sections further down the page. For the hero, load it straight away and give it priority:

{%- comment -%} Hero: usually the LCP element, so fetch it first {%- endcomment -%}
{{ section.settings.image | image_url: width: 1600 | image_tag: loading: 'eager', fetchpriority: 'high', sizes: '100vw' }}

{%- comment -%} Product grid further down: lazy-load {%- endcomment -%}
{{ product.featured_image | image_url: width: 600 | image_tag: loading: 'lazy' }}

If a section can appear anywhere, base the decision on section.index rather than hard-coding it.

Load scripts only where they’re needed

A single theme.js that holds every feature makes every page pay for all of them. Split it into modules and load each one on the templates that use it:

<script type="module" src="{{ 'product-gallery.js' | asset_url }}"></script>

Module scripts are deferred by default, so they don’t block rendering. Use defer on classic scripts.

Apps are often a bigger cost than theme code. Check which app embeds and blocks load on every page, switch them off where they aren’t used, and replace any that inject heavy scripts for a small feature.

Load non-critical CSS without blocking

Inline the small amount of CSS needed for the top of the page, then load the rest without blocking the first render:

<style>{% render 'critical-styles' %}</style>
<link rel="stylesheet" href="{{ 'deferred.css' | asset_url }}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="{{ 'deferred.css' | asset_url }}"></noscript>

This adds build complexity, because the critical CSS has to stay in step with the theme. Do it when render-blocking CSS is a measured problem, not by default.

Use render over include

include is deprecated. Shopify’s reason is that the way it shares variables reduces performance and makes code harder to read and maintain. render gives each snippet its own scope, so you pass in what it needs:

{% render 'icon-star', filled: true %}

Clean up theme.liquid

  • Remove scripts and stylesheets nothing uses any more
  • Output {{ content_for_header }} once, in the <head>
  • Keep the DOM small, especially markup repeated inside loops

A real example

On a Shopify Plus rebuild for a UK jeweller, the theme was built from modular sections and reusable templates rather than layers of patched snippets. The site handled seasonal campaign traffic without issues, and the marketing team could build pages without a developer.

The checklist

  • Keep snippet nesting shallow
  • Narrow collections before you loop over them
  • Do array work once, outside the loop
  • Lazy-load below the fold, prioritise the hero image
  • Load scripts and app embeds only where they’re used
  • Re-test with Theme Inspector and PageSpeed Insights after each change

Liquid clean-up like this is part of the Shopify performance fixes I do, on my own themes and on ones another developer built.

Published , updated .

Part 1 of Shopify Essentials.

Related posts