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:
- Identify the problem — slow render, unexpected output, blank variables
- Log relevant variables — use
{% debug %}to inspect values safely - Check the web console — the web console shows debug output (never the page, and never the browser’s console)
- Profile hotspots — use
{% timer %}to measure execution time - Locate bottlenecks — identify which sections are slow
- Optimize or cache — fix the slow section or wrap in
{% cache %} - 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?
Thanks for your feedback! It helps us improve our docs.