Skip to content
Log in

Liquid debugging and performance guide

On this page

Learn to debug Liquid templates effectively and measure performance to identify and fix slow sections. This guide covers safe logging practices, performance profiling, and optimization patterns.

Debugging workflow

A structured approach to debugging:

  1. Identify the problem — slow render, unexpected output, blank variables
  2. Log relevant variables — use {% debug %} to inspect values safely
  3. Check the web console — the web console shows debug output (never the page, and never the browser’s console)
  4. Profile hotspots — use {% timer %} to measure execution time
  5. Locate bottlenecks — identify which sections are slow
  6. Optimize or cache — fix the slow section or wrap in {% cache %}
  7. Remove debug tags — strip all debug/timer before shipping to production

Safe logging with debug tag

The {% debug %} tag writes to the web console only, StoreConnect’s own developer console rather than the browser’s. It never appears in page output and never reaches the visitor’s browser. It also records nothing unless the console was already connected when the page loaded, which is one common reason a debug tag appears to do nothing. Two others silence it just as quietly, a controller block that never ran and a cached fragment that hit. The debug tag reference covers how to tell all three apart.

Safe logging patterns

```liquid

{% debug product_id: product.id, price: product.pricing.price %} ```

This outputs to console but not the page.

What NOT to log

Never log full objects or sensitive data:

```liquid

{%- comment -%} ✗ DO NOT DO THIS {%- endcomment -%} {% debug customer: current_customer %} {% debug cart: current_cart %} {% debug params: current_request.params %} ```

These expose: - Customer IDs and personal data - Cart contents and pricing - All request parameters (including secrets)

Safe alternatives

Log only the fields you need:

```liquid

{%- comment -%} ✓ CORRECT {%- endcomment -%} {% debug customer_id: current_customer.id, country: current_customer.billing_address.country %} {% debug cart_total: current_cart.totals.total, item_count: current_cart.items.size %} ```

Production debugging

Debug tags in production are silent no-ops if no console is open. However, always remove them before shipping:

```bash

grep -r “{% debug” theme/ –exclude-dir=node_modules ```

Performance profiling with timer

Use {% timer %} to measure how long a block takes to render.

Simple timer

```liquid

{% timer “product_render” %} {% render “products/card”, product: product %} {% endtimer %} ```

Output in console: product_render: 45.23ms

Comparing before/after

Measure the same section twice to see improvement:

```liquid

{% timer “before_optimization” %} {% for product in all_products limit: 100 %} {{ product.name }} {% endfor %} {% endtimer %}

{%- comment -%} after optimization {%- endcomment -%} {% timer “after_optimization” %} {% for product in all_products limit: 100 %} {{ product.name | upcase }} {% endfor %} {% endtimer %} ```

Nested timers

Profile multiple levels:

```liquid

{% timer “render_page” %} {% timer “query” %} {% query ‘Product2’ as products, s_c__store__c: current_store.sfid limit: 50 %} {% endtimer %}

{% timer “loop_and_render” %} {% for product in products %} {% render “products/card”, product: product %} {% endfor %} {% endtimer %} {% endtimer %} ```

Console output: query: 125.45ms loop_and_render: 89.34ms render_page: 220.12ms

Interpreting results

  • Query time > 100ms — consider a more efficient query or add a filter
  • Render loop > 200ms — consider caching the loop output
  • Each render > 20ms — snippet is expensive; consider caching individual items

Cache strategy patterns

Performance improvements usually come from caching, not code optimization.

Pattern 1: Cache expensive queries

```liquid

{% cache “products_featured”, items: [current_store], expires_in: 300 %} {% query ‘Product2’ as products, s_c__store__c: current_store.sfid, data.featured__c: true, isactive: true %}

{%- for product in products -%} {{ product.name }}: {{ product.pricing.price | money }} {%- endfor -%} {% endcache %} ```

Cache time: 5 minutes. Re-run query every 5 minutes only.

Pattern 2: Cache per-product

```liquid

{% for product in all_products limit: 20 %} {% cache “product_card”, items: [product, current_store, current_customer], expires_in: 600 %} {% render “products/card”, product: product %} {% endcache %} {% endfor %} ```

Each product gets its own cached copy, valid for 10 minutes.

Pattern 3: Cache navigation

```liquid

{% cache “header_nav”, items: [current_store], expires_in: 3600 %} {% render “shared/navigation” %} {% endcache %} ```

Navigation cache: 1 hour. Expires when Store’s Cache Version changes.

Pattern 4: Cache with customer personalization

```liquid

{% cache “sidebar”, items: [current_store, current_customer], expires_in: 600 %} {% render “shared/sidebar” %} {% endcache %} ```

Each customer gets their own sidebar cache. Refreshes every 10 minutes or when cart changes.

Pattern 5: Skip cache for sensitive content

Never cache: - Checkout state - Payment forms - Consent controls - Account information - Promotional codes (cart-dependent)

Pattern 6: Cache small pieces

```liquid

{%- comment -%} Good: cache the card, not the whole loop {%- endcomment -%} {% for product in products %} {% cache “product_card_{{ product.id }}”, items: [product], expires_in: 300 %} {% render “products/card”, product: product %} {% endcache %} {% endfor %} ```

Small caches are more efficient than one large cache.

Common performance bottlenecks

1. Unscoped queries

Slow:

```liquid

{% query ‘Product2’ as all_products %} ```

This fetches ALL products. If you have 100k products, this is painfully slow.

Fast:

```liquid

{% query ‘Product2’ as products, s_c__store__c: current_store.sfid, isactive: true, data.featured__c: true %} ```

Scope by store, active status, and filters.

2. N+1 queries in loops

Slow:

```liquid

{% for category in all_categories %} {% query ‘Product2’ as products, s_c__category__c: category.id %} {{ products.size }} products {% endfor %} ```

This runs one query per category (N+1 problem).

Fast:

```liquid

{% query ‘Product2’ as products, s_c__store__c: current_store.sfid %}

{% for category in all_categories %} {%- assign category_products = products | where: “s_c__category__c”, category.id -%} {{ category_products.size }} products {% endfor %} ```

Query once, filter in memory.

3. Uncached expensive snippets in loops

Slow:

```liquid

{% for product in products %} {% render “expensive_snippet”, product: product %} {% endfor %} ```

Slow snippet rendered 100 times per page view.

Fast:

```liquid

{% for product in products %} {% cache “snippet_{{ product.id }}”, items: [product], expires_in: 600 %} {% render “expensive_snippet”, product: product %} {% endcache %} {% endfor %} ```

Each product’s snippet is cached.

4. Large array operations

Slow:

```liquid

{% assign all_options = “” | split: “” %} {% for product in all_products limit: 1000 %} {% for variant in product.variants %} {% assign all_options = all_options | push: variant.option_values %} {% endfor %} {% endfor %} ```

Array operations are expensive at scale.

Fast:

```liquid

{% assign options = all_products | map: “variants” | map: “option_values” | flatten | uniq %} ```

Use filters instead of loops.

Production considerations

Removing debug before deploy

```bash

Find all debug tags

grep -r “{% debug” docs/

Find all timer tags

grep -r “{% timer” docs/

Remove them with sed (backup first!)

sed -i.backup ‘s/{% debug.*%}//g’ file.liquid ```

Monitoring performance

On production, monitor: - Page render times (goal: < 1s) - Query execution (goal: < 100ms each) - Cache hit rate (goal: > 80% for frequently-used caches)

Performance budgets

Set budgets for each page: - Homepage — < 1s render time - Product page — < 500ms render time - Search results — < 800ms render time

If a page exceeds its budget, profile with timers and cache the hotspot.

Was this article helpful?

Was this article helpful?