I diagnosed a Shopify Plus storefront last quarter sitting at p75 TTFB 2.4 seconds on mobile. The merchant assumed Shopify’s CDN was slow. It wasn’t. The collection template had a nested Liquid for loop running over all_products with metafield reads inside the inner loop. 62,000 iterations on every page load. Single PR rewrite, TTFB dropped to 580ms in 24 hours. The 4 patterns below cover every Liquid-side TTFB issue I see in 2026 audits.
TL;DR: Shopify CDN TTFB is rarely the problem. The 4 Liquid patterns that push TTFB past 1.8s: nested for loops, all_products iteration, metafield N+1 reads, deep section recursion. Each is auditable via ?profile=1 and fixable in a single PR. Median Shopify Plus p75 TTFB is 400-800ms; stores past 1.5s almost always have a Liquid issue.
Why TTFB matters separately from LCP
- TTFB (Time to First Byte) is the green-band metric Google uses as a Core Web Vital input. Under 800ms is the fast threshold.
- TTFB sits inside the LCP measurement; a 1.6s TTFB caps LCP at 1.6s minimum. Every millisecond of server-render time is a millisecond LCP cannot recover.
- TTFB is the only metric the customer experiences before any pixels paint. 2.4s of blank screen is the felt cost on the storefront.
The Shopify CDN handles the network and cache layers reliably. What it cannot fix is a 1,400ms Liquid render. That part is on the theme.
Killer 1: nested for loops over large collections
The audit case:
{%- comment -%} BAD: nested loop with metafield reads {%- endcomment -%}
{%- for product in collections.all.products -%}
{%- for variant in product.variants -%}
{%- if variant.metafields.custom.featured == 'true' -%}
...
{%- endif -%}
{%- endfor -%}
{%- endfor -%}
On a store with 250 products averaging 5 variants each, the outer loop runs 250 times. The inner loop runs 5 times per outer iteration. The metafield read inside fires 1,250 times per page load. Shopify’s Liquid runtime caps collections.all.products at 250 items by default, but that cap is the upper bound, not a fix; even 250 outer iterations is too many for synchronous render.
The fix is moving the filter to a metafield-based collection or a smart collection that pre-computes membership:
{%- assign featured = collections.featured-variants.products -%}
{%- for product in featured limit: 12 -%}
...
{%- endfor -%}
Smart collections evaluate membership at write time in Shopify Admin, not on every page render. Render time drops from 800-1,400ms to 40-90ms on the audit case.
Killer 2: all_products iteration
collections.all.products is the catalog dump. Iterating it on a page that does not need the full catalog is the most common TTFB killer. Common offenders:
- “Featured products” sections that pull all_products and filter by tag in Liquid (should use a tagged smart collection)
- Search index pre-builds that iterate all_products to emit a JSON blob (should use the Search & Discovery API)
- Variant-level option counters that loop all_products to count colour options (should pre-aggregate via metafield)
The smell test: any for product in collections.all.products on a non-search template is suspect. Audit it against the actual data need. If you need 12 products, fetch 12, not 250.
Killer 3: metafield N+1 reads
Liquid metafield reads look free in the source but cost real time at render. Shopify’s Liquid runtime fetches metafields per-object on first access. Reading product.metafields.custom.spec_sheet inside a for product in collection.products loop fires one metafield query per product, per page render.
The fix is the metafields_data preload pattern via Liquid’s batch metafield access, or restructuring the data into a single metaobject that pre-aggregates the values:
{%- comment -%} BEFORE: N+1 metafield reads {%- endcomment -%}
{%- for product in collection.products -%}
<span>{{ product.metafields.custom.warranty }}</span>
{%- endfor -%}
{%- comment -%} AFTER: read once, render from cache {%- endcomment -%}
{%- assign warranty_lookup = shop.metafields.custom.warranty_by_product_handle.value -%}
{%- for product in collection.products -%}
<span>{{ warranty_lookup[product.handle] }}</span>
{%- endfor -%}
The metaobject approach pre-aggregates the values in admin. The lookup at render time is a single object dereference per product, not a metafield query.
For the broader Shopify metafield patterns, see my Shopify metaobjects vs metafields post.
Killer 4: deep section recursion in JSON templates
OS 2.0 JSON templates let you nest sections inside sections via app blocks and block schema. Each nesting level costs render time. A homepage with 20 sections, each with 5-10 blocks, each invoking 2-3 snippets via {% render %}, can hit 600-1,200ms of pure Liquid render time even before any data fetching.
The audit move:
- Hit
?profile=1on the storefront URL while logged in as store owner. Shopify renders a profiler at page bottom showing every section’s render time. - Sort by render time descending.
- Anything above 50ms per section deserves audit. Above 200ms is the bottleneck.
The common culprits in audit:
- A “trust badges” section rendering 8 SVG icons via
{% render %}with parameters (each render is parsed + executed separately; should be inline) - A “featured products” section rendering full product cards with 4-5 metafield reads each (should use a lightweight card snippet)
- A “blog feed” section iterating
articleswith full HTML rendering (should use a list with linked headings only)
Refactor the heaviest section first. One section at 400ms is usually the biggest gain you can ship in one PR.
How to audit your TTFB in 5 minutes
Three checks, in this order.
curl -w:
curl -w '%{time_starttransfer}' -o /dev/null -s https://yourstore.com/products/best-seller
Returns the TTFB in seconds. Under 0.8 is green, 0.8-1.8 needs improvement, above 1.8 is the diagnosis zone.
?profile=1: log in as the store owner, append ?profile=1 to any URL. Shopify renders the Liquid profiler at the bottom of the page. Sort sections by render time. The top 3 sections by render time are your refactor targets.
CrUX field data: PageSpeed Insights shows p75 TTFB in the Field Data section, rolling 28-day window. Day 1 looks pre-fix; day 28 reflects the new value.
For the broader Core Web Vitals optimisation across LCP plus INP plus CLS plus TTFB, see my Shopify Core Web Vitals 2026 guide and the CrUX Grader tool.
What this does not fix
Three TTFB patterns the 4 fixes above do not solve:
- Signed-in customer pages. These bypass the edge cache. TTFB depends entirely on Liquid render time, with no CDN buffer. Optimising the Liquid is the only lever.
- B2B catalogs with customer-tag pricing. Each customer sees a personalised price, which forces a Liquid-side re-render per customer. Cart and PDP TTFB will sit higher than signed-out equivalents. Acceptable trade-off for the B2B feature set.
- Apps that inject server-side code via Liquid tag injection. Some review apps and bundle apps inject Liquid that runs server-side on every page. If you cannot defer or remove the app’s Liquid injection, the TTFB cost is fixed. See my third-party script defer playbook for the JS side of the same problem.
The takeaway
- Shopify CDN is rarely the TTFB problem. Diagnose Liquid first. Under 800ms is the green target; 1.5s+ signals a Liquid render-time issue.
- Nested loops over collections.all.products are the most common killer. Move filtering to smart collections; render time drops 10-20x in the audit cases I have shipped.
- Metafield N+1 reads compound silently. Pre-aggregate via metaobjects or shop-level metafields; lookup at render time is a single dereference.
- Section recursion in JSON templates accumulates fast.
?profile=1reveals the top render-time sections. One refactor on the heaviest section usually moves p75 TTFB by 300-600ms. - Measure with curl + ?profile=1 first, CrUX over 28 days for field proof. Lab moves immediately; field is a rolling window.