All articles
E-commerceOctober 27, 2026 6 min read

Shopify Section Rendering API: The Underused Trick That Fixed Our Collection Page Jank

A field report on using Shopify's Section Rendering API to kill full-page reloads on filtered collection pages, with the gotchas we hit around caching, analytics, and infinite scroll.

Filtered collection pages are where most Shopify themes quietly lose money. A shopper taps a size filter, the whole page reloads, the hero flashes, the grid repaints, and by the third refinement they're gone. We hit this on a mid-size apparel store late last year and ended up rebuilding the filter layer on top of Shopify's Section Rendering API instead of jumping to a headless rewrite. Here's what actually worked, what broke, and when we'd skip the whole exercise.

Why full-page reloads on collections are worse than they look

On paper, a Dawn-based collection page reloading on every filter change isn't catastrophic — the HTML is cached, Shopify's CDN is fast, and LCP can still land in a reasonable range. In practice, the perceived performance is dreadful. Each refinement triggers:

  • A visible white flash between navigations
  • Re-execution of every third-party script in the head (analytics, consent, chat, A/B testing)
  • Scroll position jumping back to the top on some browsers
  • A fresh layout shift as lazy-loaded images reassert themselves

For merchants with heavy third-party stacks — and that's most of them — the time between click and "page feels usable again" is often 2–4 seconds even when the server response is under 300ms. That's where the drop-off happens. We've seen filter-interaction bounce rates in the 30–45% range on stores where the technical metrics looked fine.

The fix doesn't require going headless. Shopify has shipped the primitive for this since 2021, and it's still underused.

What the Section Rendering API actually does

The Section Rendering API lets you request a rendered Liquid section as an HTML fragment by appending ?sections=section-id to any storefront URL. Shopify runs the Liquid, applies the current URL's query string (including filter parameters), and returns a JSON object keyed by section ID with the rendered markup inside.

So instead of navigating to /collections/jackets?filter.v.option.size=M, you fetch that URL in the background, pull out the product-grid section, and swap it into the DOM. No reload, no script re-execution, no flash.

Here's the minimum viable fetch:

async function updateCollection(url) {
  const sectionId = 'product-grid'; // matches {% section 'product-grid' %}
  const response = await fetch(`${url}&sections=${sectionId}`);
  const data = await response.json();
  
  const parser = new DOMParser();
  const doc = parser.parseFromString(data[sectionId], 'text/html');
  const newGrid = doc.querySelector('#ProductGridContainer');
  
  document.querySelector('#ProductGridContainer').replaceWith(newGrid);
  history.pushState({}, '', url);
}

Dawn ships with a version of this pattern in facets.js, but it's wired specifically to the reference theme's markup. If you're on a custom theme — and most serious merchants are — you'll be reimplementing it anyway.

Why not just use Hydrogen or a headless rewrite?

Because the ROI rarely clears the bar. A headless rebuild for a store doing $5M–$20M in annual revenue typically runs six figures and 4–6 months, and the measurable conversion lift from the storefront alone is usually in the single digits. Section Rendering gets you 70% of the perceived-performance win in two sprints while keeping the entire Shopify admin, checkout, and app ecosystem intact. We've written about when Hydrogen pays off separately — for most catalogues under 50k SKUs, it doesn't.

The implementation that actually shipped

The naive version above works for ten minutes and then falls apart. Here's what we had to add before it was production-ready.

Debouncing and request cancellation

Shoppers click filters fast. If someone toggles three sizes in a second, you'll fire three parallel requests and whichever resolves last wins — which may not be the last one clicked. We use AbortController and a short debounce:

let activeController = null;
let debounceTimer = null;

function scheduleUpdate(url) {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    if (activeController) activeController.abort();
    activeController = new AbortController();
    updateCollection(url, activeController.signal);
  }, 120);
}

120ms felt right in testing — short enough to feel instant, long enough to collapse rapid clicks.

Updating the filter sidebar too

Available filter counts change when the result set changes. If you only swap the product grid, your sidebar will show "Size M (247)" when there are actually 12 matching items. The Section Rendering API accepts multiple section IDs: ?sections=product-grid,collection-filters. Fetch both, swap both.

One subtle thing: don't blow away the filter sidebar's DOM state wholesale, because you'll lose focus and any open accordion panels. We diff the counts and disabled states and update attributes in place rather than replacing the whole element.

Analytics events

This one bit us. Because you're not navigating, GA4, Meta Pixel, and whatever else the merchant has installed won't fire page views automatically. For filtered collection views you probably want a view_item_list event, not a page_view, but the specific choice depends on how the merchant's analytics are modelled. The important thing is to fire something explicitly after each swap, and to pass the new filter state as parameters so the data team can actually use it.

Infinite scroll vs pagination

If you're going to the trouble of partial rendering, pagination feels archaic. Infinite scroll on top of Section Rendering is straightforward — fetch page 2 with &page=2&sections=product-grid, append rather than replace — but mind two things: keep a real <a href> to the next page for crawlers and no-JS fallback, and update the URL as users scroll so sharing and back-button behaviour work. IntersectionObserver handles the trigger cleanly.

The gotchas nobody mentions

A few things that cost us time and would have been nice to know upfront.

Shopify's CDN caches Section Rendering responses aggressively. That's great for performance, bad when you're debugging why a Liquid change isn't showing up. Append a cache-busting query param during development.

The returned HTML is a string, not a document fragment. Parsing it with DOMParser works but any <script> tags inside won't execute when inserted via innerHTML or replaceWith. If your section relies on inline scripts for product cards (quick-add, swatches, etc.), you need to either re-execute them manually or — better — move that logic into web components that initialise on connectedCallback.

Metafield-driven content can be expensive. If each product card pulls three metafields and you're rendering 48 per page, the server-side Liquid render time can creep past 500ms. Preload metafields on the collection object where possible, and consider whether you really need all that data in the grid view versus the PDP.

SEO is unaffected but only if you keep real URLs. Google indexes the server-rendered HTML at each filter URL. The AJAX layer is pure progressive enhancement — if JS fails, the links still work as full-page navigations. Don't be tempted to hijack with preventDefault on anchors that have no href.

What we measured

I'll keep this honest: we didn't run a clean A/B test because the merchant wanted the fix shipped before Black Friday. What we observed in the four weeks after launch, versus the four weeks before:

  • Time between filter click and interactive grid: dropped from a 2–3 second range to roughly 300–500ms
  • Filter engagement (shoppers using 2+ filters per session): up meaningfully
  • Collection-to-PDP click-through: up in the mid single digits
  • Revenue per collection session: up, but the attribution is noisy because we also shipped a merchandising change the same week

Treat those as directional, not benchmarks. The point is that the perceived-performance delta was large enough that merchandisers noticed before the analytics did.

Where we'd start

If you're staring at a slow filtered collection page on Shopify, don't start with a headless RFC. Start here:

  1. Audit how many third-party scripts fire on navigation. That's usually the real villain, and Section Rendering sidesteps most of it.
  2. Prototype the swap for one section on one collection. A day's work, enough to feel the difference.
  3. Wire up abort controllers, analytics events, and the filter sidebar update before you ship anywhere near production.
  4. Keep every filter combination as a real, crawlable URL. Progressive enhancement, always.

If after all that the page still feels slow, the bottleneck is probably Liquid render time or app bloat — and those are worth fixing before you spend six months on a platform migration. If you want a second pair of eyes on a storefront that's underperforming, our e-commerce engineering team does this kind of audit regularly.

#Shopify#Performance#CRO#Frontend

Want a team like ours?

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

Start a project