{"title":"Liquid debugging and performance guide","slug":"liquid-debugging-and-performance","url":"https://support.storeconnect.com/articles/liquid-debugging-and-performance","url_markdown":"https://support.storeconnect.com/articles/liquid-debugging-and-performance.md","subtitle":null,"summary":"Debug Liquid templates safely and profile performance bottlenecks. Use the debug and timer tags to optimize rendering, identify slow queries, and monitor production behavior.","type":"Developer_Documentation","video_url":"","keywords":"liquid debugging, timer tag, debug tag, performance profiling, liquid optimization, storeconnect liquid","last_modified":"2026-09-15T02:32:04+0000","body_markdown":"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.\n\n## Debugging workflow\n\nA structured approach to debugging:\n\n1. **Identify the problem** — slow render, unexpected output, blank variables\n2. **Log relevant variables** — use `{% debug %}` to inspect values safely\n3. **Check the web console** — the [web console](web-console) shows debug output (never the page, and never the browser's console)\n4. **Profile hotspots** — use `{% timer %}` to measure execution time\n5. **Locate bottlenecks** — identify which sections are slow\n6. **Optimize or cache** — fix the slow section or wrap in `{% cache %}`\n7. **Remove debug tags** — strip all debug/timer before shipping to production\n\n## Safe logging with debug tag\n\nThe `{% debug %}` tag writes to the [web console](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](debug-tag-reference) covers how to tell all three apart.\n\n### Safe logging patterns\n\n\n```liquid\n\n{% debug product_id: product.id, price: product.pricing.price %}\n```\n\n\nThis outputs to console but not the page.\n\n### What NOT to log\n\nNever log full objects or sensitive data:\n\n\n```liquid\n\n{%- comment -%} ✗ DO NOT DO THIS {%- endcomment -%}\n{% debug customer: current_customer %}\n{% debug cart: current_cart %}\n{% debug params: current_request.params %}\n```\n\n\nThese expose:\n- Customer IDs and personal data\n- Cart contents and pricing\n- All request parameters (including secrets)\n\n### Safe alternatives\n\nLog only the fields you need:\n\n\n```liquid\n\n{%- comment -%} ✓ CORRECT {%- endcomment -%}\n{% debug customer_id: current_customer.id, country: current_customer.billing_address.country %}\n{% debug cart_total: current_cart.totals.total, item_count: current_cart.items.size %}\n```\n\n\n### Production debugging\n\nDebug tags in production are silent no-ops if no console is open. However, **always remove them before shipping**:\n\n\n```bash\n\ngrep -r \"{% debug\" theme/ --exclude-dir=node_modules\n```\n\n\n## Performance profiling with timer\n\nUse `{% timer %}` to measure how long a block takes to render.\n\n### Simple timer\n\n\n```liquid\n\n{% timer \"product_render\" %}\n  {% render \"products/card\", product: product %}\n{% endtimer %}\n```\n\n\nOutput in console: `product_render: 45.23ms`\n\n### Comparing before/after\n\nMeasure the same section twice to see improvement:\n\n\n```liquid\n\n{% timer \"before_optimization\" %}\n  {% for product in all_products limit: 100 %}\n    {{ product.name }}\n  {% endfor %}\n{% endtimer %}\n\n{%- comment -%} after optimization {%- endcomment -%}\n{% timer \"after_optimization\" %}\n  {% for product in all_products limit: 100 %}\n    {{ product.name | upcase }}\n  {% endfor %}\n{% endtimer %}\n```\n\n\n### Nested timers\n\nProfile multiple levels:\n\n\n```liquid\n\n{% timer \"render_page\" %}\n  {% timer \"query\" %}\n    {% query 'Product2' as products, s_c__store__c: current_store.sfid limit: 50 %}\n  {% endtimer %}\n  \n  {% timer \"loop_and_render\" %}\n    {% for product in products %}\n      {% render \"products/card\", product: product %}\n    {% endfor %}\n  {% endtimer %}\n{% endtimer %}\n```\n\n\nConsole output:\n```\nquery: 125.45ms\nloop_and_render: 89.34ms\nrender_page: 220.12ms\n```\n\n### Interpreting results\n\n- **Query time \u003e 100ms** — consider a more efficient query or add a filter\n- **Render loop \u003e 200ms** — consider caching the loop output\n- **Each render \u003e 20ms** — snippet is expensive; consider caching individual items\n\n## Cache strategy patterns\n\nPerformance improvements usually come from caching, not code optimization.\n\n### Pattern 1: Cache expensive queries\n\n\n```liquid\n\n{% cache \"products_featured\", items: [current_store], expires_in: 300 %}\n  {% query 'Product2' as products,\n      s_c__store__c: current_store.sfid,\n      data.featured__c: true,\n      isactive: true %}\n  \n  {%- for product in products -%}\n    {{ product.name }}: {{ product.pricing.price | money }}\n  {%- endfor -%}\n{% endcache %}\n```\n\n\nCache time: 5 minutes. Re-run query every 5 minutes only.\n\n### Pattern 2: Cache per-product\n\n\n```liquid\n\n{% for product in all_products limit: 20 %}\n  {% cache \"product_card\", items: [product, current_store, current_customer], expires_in: 600 %}\n    {% render \"products/card\", product: product %}\n  {% endcache %}\n{% endfor %}\n```\n\n\nEach product gets its own cached copy, valid for 10 minutes.\n\n### Pattern 3: Cache navigation\n\n\n```liquid\n\n{% cache \"header_nav\", items: [current_store], expires_in: 3600 %}\n  {% render \"shared/navigation\" %}\n{% endcache %}\n```\n\n\nNavigation cache: 1 hour. Expires when Store's Cache Version changes.\n\n### Pattern 4: Cache with customer personalization\n\n\n```liquid\n\n{% cache \"sidebar\", items: [current_store, current_customer], expires_in: 600 %}\n  {% render \"shared/sidebar\" %}\n{% endcache %}\n```\n\n\nEach customer gets their own sidebar cache. Refreshes every 10 minutes or when cart changes.\n\n### Pattern 5: Skip cache for sensitive content\n\nNever cache:\n- Checkout state\n- Payment forms\n- Consent controls\n- Account information\n- Promotional codes (cart-dependent)\n\n### Pattern 6: Cache small pieces\n\n\n```liquid\n\n{%- comment -%} Good: cache the card, not the whole loop {%- endcomment -%}\n{% for product in products %}\n  {% cache \"product_card_{{ product.id }}\", items: [product], expires_in: 300 %}\n    {% render \"products/card\", product: product %}\n  {% endcache %}\n{% endfor %}\n```\n\n\nSmall caches are more efficient than one large cache.\n\n## Common performance bottlenecks\n\n### 1. Unscoped queries\n\n❌ **Slow:**\n\n```liquid\n\n{% query 'Product2' as all_products %}\n```\n\n\nThis fetches ALL products. If you have 100k products, this is painfully slow.\n\n✅ **Fast:**\n\n```liquid\n\n{% query 'Product2' as products,\n    s_c__store__c: current_store.sfid,\n    isactive: true,\n    data.featured__c: true %}\n```\n\n\nScope by store, active status, and filters.\n\n### 2. N+1 queries in loops\n\n❌ **Slow:**\n\n```liquid\n\n{% for category in all_categories %}\n  {% query 'Product2' as products, s_c__category__c: category.id %}\n  {{ products.size }} products\n{% endfor %}\n```\n\n\nThis runs one query per category (N+1 problem).\n\n✅ **Fast:**\n\n```liquid\n\n{% query 'Product2' as products, s_c__store__c: current_store.sfid %}\n\n{% for category in all_categories %}\n  {%- assign category_products = products | where: \"s_c__category__c\", category.id -%}\n  {{ category_products.size }} products\n{% endfor %}\n```\n\n\nQuery once, filter in memory.\n\n### 3. Uncached expensive snippets in loops\n\n❌ **Slow:**\n\n```liquid\n\n{% for product in products %}\n  {% render \"expensive_snippet\", product: product %}\n{% endfor %}\n```\n\n\nSlow snippet rendered 100 times per page view.\n\n✅ **Fast:**\n\n```liquid\n\n{% for product in products %}\n  {% cache \"snippet_{{ product.id }}\", items: [product], expires_in: 600 %}\n    {% render \"expensive_snippet\", product: product %}\n  {% endcache %}\n{% endfor %}\n```\n\n\nEach product's snippet is cached.\n\n### 4. Large array operations\n\n❌ **Slow:**\n\n```liquid\n\n{% assign all_options = \"\" | split: \"\" %}\n{% for product in all_products limit: 1000 %}\n  {% for variant in product.variants %}\n    {% assign all_options = all_options | push: variant.option_values %}\n  {% endfor %}\n{% endfor %}\n```\n\n\nArray operations are expensive at scale.\n\n✅ **Fast:**\n\n```liquid\n\n{% assign options = all_products | map: \"variants\" | map: \"option_values\" | flatten | uniq %}\n```\n\n\nUse filters instead of loops.\n\n## Production considerations\n\n### Removing debug before deploy\n\n\n```bash\n\n# Find all debug tags\ngrep -r \"{% debug\" docs/\n\n# Find all timer tags\ngrep -r \"{% timer\" docs/\n\n# Remove them with sed (backup first!)\nsed -i.backup 's/{% debug.*%}//g' file.liquid\n```\n\n\n### Monitoring performance\n\nOn production, monitor:\n- Page render times (goal: \u003c 1s)\n- Query execution (goal: \u003c 100ms each)\n- Cache hit rate (goal: \u003e 80% for frequently-used caches)\n\n### Performance budgets\n\nSet budgets for each page:\n- **Homepage** — \u003c 1s render time\n- **Product page** — \u003c 500ms render time\n- **Search results** — \u003c 800ms render time\n\nIf a page exceeds its budget, profile with timers and cache the hotspot."}