A Shopify store can pass every Lighthouse test on a demo product and still crawl to a stop once it has a real catalogue, a handful of apps, and a busy homepage. Nine times out of ten, the culprit isn’t the hosting, the images, or even the apps, it’s how the theme’s Liquid code is structured.
Liquid is Shopify’s templating language, and it runs on Shopify’s servers every time a page is requested. That means inefficient Liquid doesn’t just make code messy, it directly adds to server response time and, further down the chain, to what a shopper actually waits for before they can browse or buy.
This post walks through the three Liquid mistakes we see most often in Shopify themes: nested loops over collections and products, unnecessary or repeated object and filter calls, and render-blocking snippet includes. You’ll get a practical way to spot each one in a theme’s code, plus a step-by-step checklist you can run yourself before deciding whether the fix needs a developer.
A Quick Primer: How Liquid Actually Renders a Page
Before diagnosing mistakes, it helps to be precise about what’s happening under the hood.
Every Shopify Online Store 2.0 theme is built from sections (reusable, configurable blocks of content that appear on templates, like a hero banner or a product grid) and snippets (smaller, reusable chunks of Liquid code that sections and other snippets call into, like a price display or a product card). When a shopper requests a page, Shopify’s servers assemble the relevant JSON template, pull in the sections it references, resolve every snippet call, run all the Liquid logic and object lookups, and return a finished HTML document.
Two tags are used to pull a snippet into a section or another snippet:
{% render 'snippet-name' %}, the current, recommended tag. A rendered snippet gets its own variable scope: it can only see variables you explicitly pass into it, and anything it defines internally doesn’t leak back out. This makes behaviour predictable and easier to reason about.{% include 'snippet-name' %}, the older, deprecated tag. An included snippet shares the full variable scope of the template that calls it, which is exactly why Shopify has deprecated it: it’s harder to debug, harder to optimise, and more prone to accidental variable collisions in larger themes.
Every one of those render calls, loop iterations, and object lookups happens server-side, inside a single request. Shopify also applies internal boundaries on how much a single Liquid render can do, the exact thresholds aren’t something Shopify publishes in precise detail, and they can change, so we won’t quote a specific number here. The practical point for theme development is the same regardless of the exact figures: a page that nests loops inside loops, or that repeatedly queries large objects, is working the render engine harder than it needs to, and in extreme cases can push toward those internal limits and throw errors rather than just being slow.
Mistake 1: Nested Loops Over Collections and Products
The single most common performance mistake we see in custom or heavily modified themes is a loop inside a loop, typically something like looping over every collection on the store, and then looping over every product inside each of those collections, to build a mega menu, a “shop by category” grid, or a featured-products carousel.
A simplified (and problematic) example:
{% for collection in collections %}
<h3>{{ collection.title }}</h3>
{% for product in collection.products %}
<span>{{ product.title }}</span>
{% endfor %}
{% endfor %}
On a store with a small catalogue this runs fine. On a store with dozens of collections and thousands of products, this is doing an enormous amount of unnecessary work on every single page load, even though the output is often just a static-looking menu that barely changes week to week.
The fix is almost never “loop more carefully”, it’s “don’t loop over the live catalogue at all” for content that doesn’t need to be:
- For navigation and mega menus, build the structure from Shopify’s native Menus (linklists) rather than looping through
collections. Menus are already curated and don’t require iterating the full product catalogue. - Cap iteration explicitly with the
limitfilter (collection.products limit: 8) wherever you only need a preview, not the full set. - Where you genuinely need cross-collection product data on a page, consider whether it can be fetched once and reused, rather than re-queried inside every loop iteration.
- Be especially wary of nested loops inside sections that appear on every template (headers, footers, announcement bars), the cost is multiplied across every page view on the store, not just one template.
Mistake 2: Repeated Calls to Large Objects and Expensive Filters Inside Loops
The second mistake is closely related: calling large, “whole store” Liquid objects, most commonly all_products or collections, repeatedly, especially from inside a loop, instead of once.
all_products and collections are global objects that give access to the entire product and collection catalogue by handle. They’re genuinely useful for looking up a specific, known item (for example, pulling in one hero product by handle on a landing page). They become a problem when a theme calls them inside a loop, effectively asking Shopify to resolve a lookup against the whole catalogue on every single iteration:
{% for item in cart.items %}
{% assign related = all_products[item.product.handle] %}
...
{% endfor %}
Each of those lookups adds real cost, and it adds up fast on a cart with many line items, or a page that runs this pattern in more than one place.
The same principle applies to Liquid filters that do non-trivial work, string manipulation filters run repeatedly over large arrays, or filters chained many times over the same object inside a loop. A filter that’s cheap once becomes expensive when it’s re-run on every iteration of a loop that has hundreds of passes.
Practical fixes:
- Move object lookups outside loops wherever the value doesn’t change per iteration, and assign it once with
{% assign %}. - Use the specific object you actually need (
product,collection,line_item) rather than reaching forall_productsorcollectionswhen a more scoped object already has the data. - Avoid re-running the same filter chain multiple times on the same value across a template, assign the result once and reuse the variable.
- If a section needs the same computed value in several places, calculate it once near the top of the section and reference the variable throughout.
Mistake 3: Render-Blocking Snippets and Uncontrolled {% render %} Sprawl
The third mistake sits at the intersection of Liquid structure and front-end performance, and it’s the one that most directly affects what a shopper visually experiences.
This shows up in two related ways:
Too many snippet renders per page. Every section and app block that gets added over time tends to bring its own snippets. A homepage template can quietly accumulate dozens of {% render %} calls, icons, badges, trust bars, upsell widgets, each one small, but collectively adding real weight to the HTML output and the server-side work needed to assemble it. Themes that have been customised by multiple developers or agencies over the years are especially prone to this: nobody removes the old snippet when a section gets replaced, so the theme quietly carries dead weight.
Synchronous script and stylesheet loading through snippets. This is the bigger visible performance hit. It’s common to find a snippet that outputs something like:
<script src="{{ 'custom-slider.js' | asset_url }}"></script>
<link rel="stylesheet" href="{{ 'custom-slider.css' | asset_url }}">
placed directly in the document <head> or high up in the body, with no defer or async attribute on the script tag, and the stylesheet loaded render-blocking rather than deferred where it’s safe to do so. The browser has to stop and fetch, and in the script’s case often execute, that resource before it can continue building the page, and if that snippet is included on every template via the theme layout, every page pays that cost, whether or not that particular page actually needs the slider.
Checklist for cleaning this up:
- Add
deferto script tags for JavaScript that doesn’t need to run before first paint (most UI-enhancement scripts fall into this category). - Load non-critical CSS asynchronously or scope stylesheet loading to only the templates or sections that actually use it, rather than the global theme layout.
- Audit
theme.liquidand any global sections (header, footer) specifically for asset tags, this is where render-blocking resources most commonly hide, because they run on every page. - Periodically review how many distinct snippets a template renders, and remove ones tied to sections or features that are no longer live on the store.
A Practical Liquid Performance Audit Checklist
Run through this sequence on any theme you suspect has Liquid performance issues:
- Search the codebase for nested
{% for %}tags. Flag any loop that iterates overcollections,all_products, orproductsinside another loop. - Search for
all_productsandcollectionsusage. For each instance, confirm it’s called once, outside any loop, and only when a specific known item is genuinely needed. - Check
theme.liquidand global sections for<script>and<link rel="stylesheet">tags. Confirm scripts havedefer(orasyncwhere appropriate) and stylesheets aren’t blocking render unnecessarily. - Count
{% render %}calls per key template (homepage, collection, product). A high and growing count over time is a sign of snippet sprawl worth reviewing. - Confirm the theme uses
{% render %}, not{% include %}. Any remaining{% include %}tags are a legacy pattern worth migrating during your next round of theme work. - Run Shopify’s Theme Check tool (bundled with Shopify CLI) across the codebase, it automatically flags a number of these patterns, including deprecated tags and some render-blocking asset patterns, and is worth running before and after any theme change.
None of these fixes require touching the store’s design. They’re structural, under-the-hood changes, which is exactly why they’re so often missed by teams focused on visual polish.
When to Bring In a Specialist
Diagnosing a nested loop in a small snippet is one thing. Untangling years of accumulated theme customisations, working out which snippets are genuinely still in use, and re-architecting a header that’s grown into a performance bottleneck is a different scale of job, and it’s easy to break something visually while trying to fix something structurally. If a theme audit turns up multiple issues like the ones above, or you’re not confident making changes to theme.liquid without a rollback plan, that’s exactly the kind of work our Shopify web design and development team handles, going through the theme’s Liquid line by line, fixing the structural issues, and testing that nothing on the storefront breaks in the process.
FAQ
Does Liquid code actually affect Core Web Vitals and page speed scores?
Yes, indirectly but meaningfully. Liquid runs server-side, so inefficient Liquid mainly adds to server response time (which affects metrics like Time to First Byte), while render-blocking scripts and stylesheets output by Liquid directly affect client-side metrics like Largest Contentful Paint. The two problems compound each other on a slow theme.
Is {% include %} still going to work, or do I need to change it urgently?
{% include %} still functions in existing themes, but it’s a deprecated tag and Shopify recommends {% render %} for all new and updated code. There’s no need to panic-migrate an entire theme overnight, but any snippet you’re touching for other reasons is a good candidate to update to {% render %} at the same time.
Can apps cause the same kind of Liquid performance problems?
Yes. Many apps inject their own snippets, app blocks, or script tags into a theme, and they’re subject to the same issues, nested logic, uncontrolled object calls, and render-blocking scripts, just outside your direct control. It’s worth periodically auditing installed apps and removing ones that are no longer actively used, since their code often stays in the theme even after the app is disabled.
How do I know if my theme is actually hitting Shopify’s Liquid limits, rather than just being generally slow?
If a page throws a Liquid error referencing render limits, or specific templates fail to load intermittently under load, that’s a strong signal you’re close to or exceeding internal boundaries rather than just running a slow-but-working theme. In most cases, though, the symptom is simply a slow page rather than an outright error, which is why a proper code review is more reliable than waiting for something to break.
Will fixing these Liquid mistakes definitely make my store faster?
It will remove one class of performance bottleneck, but it’s rarely the only factor. Image sizing, app script bloat, and hosting/CDN behaviour also contribute to overall speed. Fixing Liquid inefficiencies is usually a meaningful, measurable improvement, but it’s worth treating as part of a broader performance review rather than a single silver bullet.
Ready to Find Out What’s Actually Slowing Your Theme Down?
If you suspect your theme’s Liquid code is part of the problem but aren’t sure where to start, a Shopify audit is the fastest way to get a clear, prioritised picture of what’s actually happening under the hood. Alternatively, book a call with our team and we’ll talk through what we’d look at first.