All articles
E-commerceAugust 15, 2026 7 min read

Shopify Collection Pages at 10,000 SKUs: Faceted Filtering Without Killing TTFB

Faceted filtering on large Shopify catalogs quietly destroys collection page performance. Here's how we architect it to keep TTFB under 400ms without a full replatform.

Collection pages are where large Shopify stores quietly bleed money. Once your catalog crosses a few thousand SKUs and merchandising adds five or six filter dimensions, the page that should convert best turns into the slowest one you own. This is a walkthrough of what actually works — and what we've watched fail — when you need faceted filtering to stay fast on Shopify without ripping the platform out.

Why collection pages break at scale

A Shopify collection with 200 products and two filters is boring to build. The same page with 10,000 products, seven filters, multi-select, price ranges, and a "in stock only" toggle is a different animal. The problems compound:

  • Liquid renders the first page server-side, but every filter change hits either the Storefront API, the Search & Discovery app's filtering endpoint, or a third-party search provider.
  • Each filter combination is a distinct cache key. Cache hit rates collapse.
  • Merchandisers want custom sort orders per collection, which fights against any pre-computed index.
  • Metafield-driven filters (material, fit, capacity) explode the facet count.

We've seen TTFB on filtered collection URLs drift from ~200ms on the base collection to well over a second once four facets are applied. LCP follows it down. Bounce follows LCP.

The three architectures you're actually choosing between

When a client says "our category pages are slow," they're almost always running one of these:

  1. Native Shopify collection + Search & Discovery filters — cheapest, works up to a point, but the filter endpoint is not something you control.
  2. Native collection + a search app (Algolia, Searchanise, Klevu, Boost) — fast queries, but you inherit their JS bundle and their rendering model.
  3. Headless collection page on Hydrogen or Next.js hitting the Storefront API's search connection or a search vendor directly.

Each has a failure mode. Let's go through what to do inside each before you consider jumping to the next.

Fixing native Shopify collections first

Before anyone touches Hydrogen, exhaust the native path. In our experience, a well-tuned native collection page can hit sub-2s LCP on 4G for catalogs up to roughly 5,000 products with 4–5 filters.

The wins that matter:

  • Paginate at 24–36, not 48+. Shopify's Liquid rendering time scales roughly linearly with product count on the page. Dropping from 48 to 24 typically shaves 150–300ms off server render time.
  • Kill the mega-swatch pattern. Rendering every colour variant as a swatch on the card means N × variants images preloaded. Render up to 4 swatches plus a +3 chip.
  • Preload only the first row's images. Everything below the fold gets loading="lazy" and fetchpriority="low". The hero product image on the first card gets fetchpriority="high".
  • Filter UI should be a <details> element or CSS-only accordion on mobile. No JS framework needed to open a filter drawer.

Here's the shape of a product card we ship as a baseline:

{% assign card_img = product.featured_image %}
<article class="pcard">
  <a href="{{ product.url }}" class="pcard__link">
    <img
      src="{{ card_img | image_url: width: 400 }}"
      srcset="{{ card_img | image_url: width: 300 }} 300w,
              {{ card_img | image_url: width: 600 }} 600w"
      sizes="(max-width: 640px) 50vw, 25vw"
      width="{{ card_img.width }}"
      height="{{ card_img.height }}"
      loading="{% if forloop.index0 < 4 %}eager{% else %}lazy{% endif %}"
      fetchpriority="{% if forloop.index0 < 2 %}high{% else %}low{% endif %}"
      alt="{{ product.title | escape }}">
  </a>
  <h3 class="pcard__title">{{ product.title }}</h3>
  <span class="pcard__price">{{ product.price | money }}</span>
</article>

Nothing exotic. But shipping this consistently across every card cuts LCP meaningfully.

When Search & Discovery starts to hurt

Shopify's native filtering is fine for boolean facets on tags and options. It struggles when you want:

  • Range filters on metafield numbers (capacity in litres, screen size).
  • "In stock in my location" as a filter.
  • Custom relevance sort per collection.
  • Filters that reflect real inventory rather than theoretical variants.

When you hit two or more of those, native filtering will keep working but every filter click will feel sluggish and merchandisers will start filing tickets. That's the signal to move.

Introducing a search index without going headless

This is the middle path most stores should sit on. Keep Liquid for the initial server render, and let a search vendor handle everything after the first paint.

The pattern:

  1. Server-render the unfiltered collection in Liquid. This gives you a fast TTFB and a real HTML page for crawlers.
  2. On filter interaction, hit the search vendor's endpoint directly from the browser and replace the grid.
  3. Update the URL with history.pushState so filtered states are shareable.
  4. On page load from a filtered URL, hit the vendor endpoint before hydrating the grid.

The tradeoff is honest: the first paint is fast and SEO-friendly, but filtered URLs render client-side. For collection pages that isn't a disaster — filtered URLs generally shouldn't be indexed anyway, and you can add robots meta accordingly.

A few implementation notes we've learned the hard way:

  • Bundle the vendor JS yourself when you can. Their default script tag often loads jQuery, polyfills, and analytics you don't want. Most vendors publish an ES module. Use it.
  • Debounce filter interactions at 150–250ms. Users click three filters in a row; you don't want three round trips.
  • Preserve scroll position on filter changes. Nothing screams "cheap site" like the page jumping to the top when someone toggles a size.
  • Skeleton the grid, don't blank it. Reserve the height of the product cards so the layout doesn't shift when results return.

When headless is actually the answer

Go headless for collection pages when at least two of these are true:

  • You have more than ~15,000 SKUs with heavy metafield-driven merchandising.
  • You need server-side rendering of filtered states for SEO on faceted URLs (rare, and usually a mistake, but sometimes real).
  • You're running multiple storefronts off one catalog and need shared components.
  • Your team already runs a Next.js or Hydrogen app for other reasons.

Headless gives you edge-cached, server-rendered filtered pages via the Storefront API's search connection or a vendor SDK on the server. Done well, TTFB stays under 400ms even on deep facet combinations because the edge cache does the heavy lifting.

Done badly, you've built a slower site with more infrastructure. The failure mode we see most often: teams put the search vendor call in a client component, so every filter change is a client-side fetch anyway, and now they're paying for Vercel plus Algolia plus a headless rebuild to get the exact same UX as the middle-path architecture above.

If you're weighing this decision, our team writes about it more in our commerce services and there's a longer breakdown of Hydrogen vs Next.js tradeoffs on the blog.

A cache strategy that survives merchandising

Once you're rendering filtered pages server-side, caching becomes the whole game. A few rules that have held up for us:

  • Cache the unfiltered collection aggressively (5–15 minutes at the edge).
  • Cache single-facet combinations for 1–2 minutes.
  • Do not cache combinations with three or more facets — the hit rate is too low to justify the memory, and stale inventory here is more visible.
  • Bust the cache on products/update webhooks, not on a timer alone.
  • Never include the cart or customer state in the collection page render. It kills shared caching.

Measuring the right thing

Core Web Vitals on collection pages get misread constantly. LCP on a grid page is usually the first product image, not the H1. If your LCP element is text, your images are loading too slowly and the browser is picking the fastest thing it can find.

What we actually watch on collection pages:

  • TTFB on filtered URLs, not just the base collection.
  • INP on filter interaction — clicking a checkbox should feel free.
  • CLS from filter panel opening — the sneaky one.
  • Add-to-cart rate from collection, if you have quick-add. This is the real business metric.

Where we'd start

If your collection pages are struggling, don't jump architectures. In order:

  1. Spend a week fixing the native Liquid template — pagination, image priorities, filter UI weight. Measure before and after on three representative filtered URLs.
  2. If native filtering is still the bottleneck, add a search vendor but keep Liquid for the first paint. Ship it behind a feature flag on one collection first.
  3. Only consider headless when the merchandising complexity, catalog size, or multi-storefront pressure genuinely justifies it — and even then, make sure your filter rendering is server-side or you've gained nothing.

The stores we see winning here aren't the ones on the most modern stack. They're the ones that made a clear-eyed call about where their bottleneck actually lives and fixed that one thing well.

#Shopify#E-commerce#Performance#CRO#Storefront

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project