TTFB Shopify

Fix TTFB in Shopify

Shopify TTFB is fundamentally different from TTFB on self-hosted stacks. You cannot install Redis, tune PHP opcache, or change the web server -- Shopify is a managed SaaS platform that runs Liquid templates on its own fleet and serves anonymous responses from a global CDN. What you can change is how much synchronous template work the storefront does on every render and which third-party apps execute on which pages. On a clean Online Store 2.0 theme like Dawn, anonymous product-page TTFB consistently lands under 350 ms. On a legacy theme with twenty installed apps, the same store can sit at 900 ms or worse without any platform-side change. This guide walks through the five highest-leverage moves merchants and theme developers can actually take inside the Shopify constraints.

Expected results

Before

880ms

TTFB (Needs Improvement) -- legacy non-2.0 theme, 22 installed apps with global Liquid injection, third-party image proxy

After

245ms

TTFB (Good) -- Dawn-based 2.0 theme, app audit (down to 9 active), section-scoped app blocks, native Shopify CDN images

Step-by-step fix

Audit installed apps and remove app-block injections

Every Shopify app typically adds either a script tag to theme.liquid (the content_for_header hook) or a Liquid snippet that renders on every page. Deactivating an app from the admin does not always remove its injected code; the Liquid fragments stay in the theme until you delete them. Inventory apps, uninstall the ones you no longer need, then open theme.liquid and the section files to remove any orphaned snippet includes that reference deleted apps.

theme.liquid -- find and remove orphaned app snippets
{%- comment -%}
  Step 1: inventory active apps
  In Shopify admin -> Apps, list everything installed.
  Anything you have not used in 30 days: uninstall (not deactivate).

  Step 2: search theme code for orphaned snippets
  In the theme editor -> Edit code, search every file for
  '{% include ' and '{% render ' references that point to
  snippet names matching uninstalled apps (e.g. 'app-block-reviews',
  'product-recommendations-app'). Delete those lines.

  Step 3: check content_for_header
  Shopify renders content_for_header on every page in theme.liquid.
  Apps inject script tags there. If you've uninstalled an app and
  still see its CDN in DevTools Network tab on a page load, the
  app left a hook -- file a removal ticket with the app vendor.
{%- endcomment -%}

<head>
  {%- comment -%} content_for_header expands all app + Shopify scripts {%- endcomment -%}
  {{ content_for_header }}

  {%- comment -%} Audit: anything else here from a deleted app? remove it {%- endcomment -%}
</head>

Convert legacy sections to Online Store 2.0 sections with lazy app blocks

Online Store 2.0 themes (Dawn, Sense, Studio, Refresh, Craft) use section-based rendering and support app blocks at the section level. Section-level app blocks only execute when the merchant has explicitly added the block to a specific section on a specific template. Legacy themes use global snippet includes in theme.liquid, which means every app block runs on every page regardless of whether the merchant is using it. Migrating to a 2.0 theme typically reduces template render time by 30 to 60 percent before any other change.

sections/product-template.liquid -- 2.0 section schema with app blocks
<!-- Online Store 2.0 section: app blocks only render when added in the editor -->
{% schema %}
{
  "name": "Product template",
  "tag": "section",
  "class": "section product-template",
  "blocks": [
    {
      "type": "title",
      "name": "Product title",
      "limit": 1
    },
    {
      "type": "price",
      "name": "Price",
      "limit": 1
    },
    {
      "type": "buy_buttons",
      "name": "Buy buttons",
      "limit": 1
    },
    {
      "type": "@app"
    }
  ],
  "default": {
    "blocks": [
      { "type": "title" },
      { "type": "price" },
      { "type": "buy_buttons" }
    ]
  }
}
{% endschema %}

<!-- Render only the blocks the merchant added in the theme editor -->
{%- for block in section.blocks -%}
  {%- case block.type -%}
    {%- when 'title' -%}
      <h1 class="product__title">{{ product.title }}</h1>
    {%- when 'price' -%}
      {%- render 'price', product: product -%}
    {%- when 'buy_buttons' -%}
      {%- render 'buy-buttons', product: product, block: block -%}
    {%- when '@app' -%}
      {%- render block -%}
  {%- endcase -%}
{%- endfor -%}

Tune Shopify CDN image transformations to avoid origin renders

Shopify ships a global image CDN with on-the-fly transforms. The fastest path is to use the native img_url filter (or image_url in 2.0) with explicit width and let the CDN serve the right size from the edge. Third-party image apps that re-process every image at request time defeat this -- they add a proxy hop and often disable Shopify's WebP and AVIF negotiation. Audit your theme for img src tags that do not use the Shopify CDN and replace them.

sections/product-template.liquid -- native CDN image with explicit dimensions
<!-- Bad: legacy syntax, full original served -->
<img src="{{ product.featured_image | img_url: 'master' }}" alt="{{ product.title }}">

<!-- Good: 2.0 image_url with width, responsive srcset, lazy below the fold -->
{%- assign img = product.featured_image -%}
<img
  src="{{ img | image_url: width: 800 }}"
  srcset="
    {{ img | image_url: width: 400 }} 400w,
    {{ img | image_url: width: 600 }} 600w,
    {{ img | image_url: width: 800 }} 800w,
    {{ img | image_url: width: 1200 }} 1200w,
    {{ img | image_url: width: 1600 }} 1600w
  "
  sizes="(min-width: 750px) 50vw, 100vw"
  width="{{ img.width }}"
  height="{{ img.height }}"
  alt="{{ product.title | escape }}"
  loading="lazy"
  decoding="async"
>

<!-- For the hero / LCP image, use loading="eager" and fetchpriority="high" instead of lazy -->

Move dynamic personalization to Shopify Hydrogen if TTFB is critical

For high-traffic stores or storefronts that need server-side personalization (custom recommendations, A/B variants, geo-pricing), Shopify Hydrogen running on Oxygen is the platform's fast path. Hydrogen is a React framework that streams server-rendered HTML from Shopify's global edge network, typically delivering TTFB under 200 ms even with personalization. The trade-off is more developer overhead than Liquid -- you maintain a Hydrogen codebase in Git, not a theme in the admin -- but the TTFB headroom is substantial.

Hydrogen -- app/routes/products.$handle.tsx (streaming server response)
// app/routes/products.$handle.tsx -- streaming product route
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {Await, useLoaderData} from '@remix-run/react';
import {Suspense} from 'react';

export async function loader({params, context}: LoaderFunctionArgs) {
  // Critical data: blocks first byte until product loads
  const product = await context.storefront.query(PRODUCT_QUERY, {
    variables: {handle: params.handle},
  });

  // Deferred data: streams in after the shell is sent (recommendations,
  // reviews, related products). TTFB is bound by the critical query only.
  const recommendations = context.storefront.query(RECS_QUERY, {
    variables: {productId: product.id},
  });

  return defer({product, recommendations});
}

export default function ProductRoute() {
  const {product, recommendations} = useLoaderData<typeof loader>();
  return (
    <>
      <ProductHero product={product} />
      <Suspense fallback={<Skeleton />}>
        <Await resolve={recommendations}>
          {(recs) => <RelatedProducts items={recs.products} />}
        </Await>
      </Suspense>
    </>
  );
}

// Deploy to Oxygen: H2_ADMIN_AUTH_TOKEN=... npx shopify hydrogen deploy
// Oxygen serves the response from a POP nearest the visitor.

Confirm Shopify CDN edge caching is hit on anonymous storefront requests

Shopify already caches anonymous storefront responses at the edge. The merchant's job is to confirm the cache is actually hit. Use curl with a clean User-Agent to fetch a product page and read the x-cache and cf-cache-status headers. A consistent MISS on simple anonymous requests usually means the theme is mutating cookies or query strings in a way that breaks cache eligibility -- common offenders are aggressive cart-token writes or app scripts that append tracking parameters server-side.

Terminal -- verify edge cache hit on a Shopify product page
# 1) Confirm the edge cache is hit on a fresh anonymous request
$ curl -sI -H 'User-Agent: Mozilla/5.0' \
    https://yourstore.myshopify.com/products/example-product | \
    grep -iE 'x-cache|cf-cache|server-timing|age'

# expected (warm):
# x-cache: HIT
# cf-cache-status: HIT
# age: 42

# 2) If x-cache is consistently MISS, look for cache-breaking response headers
$ curl -sI https://yourstore.myshopify.com/products/example-product | \
    grep -iE 'set-cookie|vary|cache-control'

# common offenders:
# - set-cookie: cart_currency=... (apps that write cookies server-side)
# - vary: cookie (too permissive)
# - cache-control: no-store (an app is overriding cache headers)

# 3) Measure TTFB across 5 runs from different regions with WebPageTest
$ npx webpagetest test https://yourstore.myshopify.com/products/example \
    --location ec2-eu-west-1:Chrome --runs 5 --first

# Healthy Shopify product-page TTFB on Dawn-based 2.0 theme: 200--350 ms warm.

Quick checklist

  • Unused Shopify apps are uninstalled (not just deactivated) and orphaned snippets removed from theme files
  • Theme is Online Store 2.0 (Dawn, Sense, Studio, Refresh, or Craft) with section-level {% schema %} blocks
  • All <img> tags use image_url with explicit width/height and a responsive srcset
  • Third-party image proxies are removed; only Shopify's native CDN serves product imagery
  • curl -I returns x-cache: HIT on an anonymous product page
  • Online Store Speed Score in admin shows no apps in the "slow apps" section

Frequently asked questions

The two biggest contributors are heavy app stacks (each installed app typically injects Liquid into theme.liquid and runs on every page render even when not visually active) and bloated legacy themes that render dozens of Liquid sections synchronously. Shopify's underlying platform consistently delivers low TTFB on a clean theme; most slow stores are slow because of merchant-installed apps and theme customizations rather than the platform itself.

No. Shopify is a fully managed SaaS platform, so there is no shell access, no PHP, no Redis, and no Nginx configuration. The platform runs Liquid templates on Shopify's own server fleet and serves anonymous responses from a global CDN. TTFB improvements come from theme and app changes that reduce server-side render time, not from infrastructure tuning.

Google rates TTFB as Good under 800 ms, Needs Improvement between 800 and 1,800 ms, and Poor above 1,800 ms. On Shopify, a clean Online Store 2.0 theme like Dawn with a small app footprint typically delivers anonymous-product-page TTFB under 350 ms. If your store is above 800 ms you almost certainly have either a heavy app stack, a legacy non-2.0 theme, or both. Shopify Hydrogen on Oxygen typically pushes TTFB under 200 ms.

Open theme.liquid and the snippet files for content_for_header and content_for_index injections, then comment out one app block at a time and run WebPageTest before and after each change. Shopify also exposes the Online Store Speed Score in admin under Storefront, which highlights which apps are flagged as slow. The Theme Inspector for Chrome reveals per-section Liquid render time and is the most reliable way to see which sections dominate template execution.

Often, yes. Online Store 2.0 themes use section-based rendering with section-level app blocks, which means app blocks only execute on pages where the merchant has explicitly added them. Legacy themes use snippet includes in theme.liquid that run on every page, even when the block is not visible. Switching from a legacy theme to Dawn typically reduces template render time by 30 to 60 percent before any other changes.

Move to Hydrogen when you have a high-traffic store (more than ~50k sessions per month), TTFB is genuinely a bottleneck (over 600 ms on a clean Liquid theme after app audit), you need server-side personalization (custom recommendations, A/B variants, geo-pricing), or you want a React-based developer workflow. For small stores under 10k sessions per month with a normal app stack, Online Store 2.0 with a 2.0 theme is faster to ship and cheaper to maintain than Hydrogen.

Continue learning