DEV Community

RAXXO Studios
RAXXO Studios

Posted on Originally published at raxxo.shop

Profiling Liquid: How I Find What Slows a Shopify Page

  • Theme Inspector shows per-section render time right in Chrome DevTools

  • Server-Timing header reveals Liquid vs network in one number

  • Nested loops over full collections are the usual slow culprit

  • Cached snippets with the render tag cut repeat work to near zero

A product page that took 900ms to render Liquid dropped to under 200ms once I found the one section eating the whole budget. The fix wasn't clever. It was a loop inside a loop over a 400-product collection. Here's how I find that kind of thing instead of guessing.

Start with the numbers the theme already gives you

Before touching any code I want one honest number: how long does Liquid take to render this page on the server? Shopify hands you that number in two places, and I check both.

The first is the Shopify Theme Inspector for Chrome. Install the extension, log into your store, open DevTools, and you get a flame graph of every Liquid partial on the page with its render time. Sections, snippets, the lot. You can see that your header took 4ms and your "recommended products" section took 310ms, and suddenly you know where to look.

The second is the Server-Timing HTTP header. Open the Network tab, click the document request, and look at the Timing breakdown. Shopify reports the Liquid processing time there as a server timing entry. This is the number that matters because it's measured on their machine, not yours. Your laptop being slow doesn't move it.

I treat these two as a pair. The header tells me the total damage. The inspector tells me which section did it.

One thing I learned the hard way: measure a warm page, not a cold one. The first request after a theme edit or a deploy can be slower because Shopify hasn't cached the compiled template yet. Load the page twice and read the second number. If I skip this step I end up "fixing" a section that was never the problem.

I also measure a real page, not a demo. A collection page with 8 products behaves nothing like the same template with 400. The slow loop only shows up under real data.

If you want the deeper context on why I keep all of this in files I can profile, see Why Every RAXXO Section Lives in Code, Not the Customizer. You can't profile what the customizer hides from you.

So the routine is simple. Open the page, read Server-Timing for the total, open the inspector for the breakdown, and write down the three slowest sections. Those three are my entire to-do list. Everything else can stay exactly as it is.

Nested loops are almost always the real cost

Once I know the slow section, the first thing I look for is a loop inside a loop. This is the single most common performance bug I find in themes, including ones I wrote.

Here's the shape of the problem. You loop over products in a collection. Inside that loop, for each product, you loop over all its variants. Inside that, maybe you loop over variant metafields. Three products and four variants is nothing. Four hundred products with six variants each is 2,400 inner iterations, and Liquid runs every one of them on the server before a single byte reaches the browser.

The fix is usually to stop doing work you don't need. If I'm showing a product card, I don't need every variant. I need the first available one and maybe the price range, and both of those are available without a loop. product.price_min and product.price_max give me the range directly. product.selected_or_first_available_variant gives me the one I actually show.

The other nested-loop trap is filtering inside the loop. I've seen code that loops over a full collection and then uses an if to show only products with a certain tag. That means Liquid still iterates the whole collection even if only 12 products match. Better to narrow the data before the loop.

Shopify's where filter helps here. collection.products | where: "type", "Poster" hands you a smaller array, and now your loop runs over 12 items instead of 400. The filter happens once. The loop runs fewer times. Both wins compound.

Limits matter too. for product in collection.products limit: 8 stops the loop after 8 iterations no matter how big the collection is. On a homepage carousel that shows 8 cards, there's no reason to render 400.

I covered the shortcuts I reach for first in The Liquid Patterns That Cut My Theme Build Time in Half. A lot of those patterns are fast on purpose, not just short.

The rule I follow: touch the smallest set of data that produces the right output. If the loop is reading more than the page displays, that's wasted server time, and the inspector will show it as a fat bar.

Cache the snippets that repeat the same work

The second big win is caching, and Shopify gives you more here than people realize.

When you use the render tag instead of include, the rendered snippet is isolated. It only sees the variables you pass it. That isolation is what makes caching safe, because the output depends only on the inputs, not on whatever global state happened to exist when it ran.

Shopify caches rendered fragments internally when it can, and you help it by keeping snippets pure. A snippet that takes a product and returns a card will cache better than one that reaches out to cart, customer, and three section settings at once. The more a snippet depends on, the less it can be reused.

So when I have a product card that renders 40 times on a collection page, I make it a clean snippet with one input:

{% render 'product-card', product: product %}

That's it. No globals, no surprise reads. Shopify can treat identical inputs as identical work.

For data that genuinely doesn't change per visitor, I lean on metafields instead of computing things in Liquid. If I need a product's "care instructions" or a pre-formatted spec table, I store it as a metafield and read it directly. No loop, no string building, no conditional logic on every render. I wrote up the patterns I trust for this in Shopify Metafields Done Right.

There's a quieter form of caching too: don't recompute inside the loop what you can compute once above it. If every iteration formats the same currency or looks up the same shop setting, hoist it out. Assign it to a variable before the loop and reference the variable inside. Forty lookups become one.

I also watch out for section settings read inside loops. Each section.settings.something is cheap on its own, but called 400 times it adds up. Read it once into a local variable at the top. Small change, measurable bar on the flame graph.

When I need to pass context between the storefront and checkout without recomputing it, I use cart attributes instead of rebuilding state. Background: Shopify Cart Attributes walks through the setup.

Measure again, then stop

The part people skip is measuring after the fix. I've convinced myself a change helped and then the Server-Timing number didn't move at all. Guessing feels like progress. The header doesn't care about feelings.

So after every change I reload twice and read the number again. If the slow section dropped from 310ms to 40ms, good, I move to the next bar. If it barely moved, I revert and look harder, because I misread the cause.

I keep a tiny log while I work. Just a text note with the section name, the before time, and the after time. On a recent collection template the three slowest sections started at 310ms, 180ms, and 90ms. After the loop and caching work they came in at 40ms, 55ms, and 45ms. The total Liquid time went from roughly 900ms to under 200ms. That's the whole story in five numbers, and I only trust it because I measured both ends.

Knowing when to stop matters as much as knowing where to start. Once a page renders Liquid in a couple hundred milliseconds, the next win usually isn't Liquid at all. It's image sizes, third-party scripts, or fonts. Those live in the browser, not on Shopify's server, and the Server-Timing Liquid number won't move them. Chasing Liquid further at that point is effort spent on the wrong layer.

A quick checklist I run before calling a page done:

  • Server-Timing Liquid under ~250ms on a warm load.

  • No section in the inspector over ~60ms unless it has a very good reason.

  • No loop reading more data than the page shows.

  • Repeated snippets use render, not include.

If all four pass, I close the tab. The page is fast enough, and the remaining milliseconds aren't worth the complexity of squeezing them out.

One more habit: I profile on a template I've already shipped, not just new work. Stores drift. A collection that was fast with 30 products gets slow at 300 without anyone changing a line of code. Re-profiling the big templates a couple of times a year catches that drift before a customer feels it.

Bottom Line

If a Shopify page feels slow, don't rewrite the theme. Open the Theme Inspector, read the Server-Timing header, and find the one section that's eating the budget. Nine times out of ten it's a nested loop over a collection that's grown too big, and the fix is narrowing the data or caching a clean snippet with render.

Start with the measurement. Everything else is guessing until you have the two numbers: total Liquid time and the per-section breakdown. Then fix the biggest bar, measure again, and stop when the page renders in a couple hundred milliseconds.

If you're building a store on Shopify and want the thinking behind how I keep themes in code where they can be profiled and versioned, the Claude Blueprint lays out the full approach I use. Profile first, fix the expensive thing, leave the rest alone.

This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)

Top comments (0)