All articles
E-commerceJune 2, 2026 6 min read

Why Your Collection Page Is Slower Than Your PDP (And How to Fix It)

Collection pages quietly sabotage discovery on most Shopify stores. Here's why they're often slower than product pages — and the engineering moves that actually fix it.

Why Your Collection Page Is Slower Than Your PDP (And How to Fix It)

Everyone obsesses over product detail pages. They run Lighthouse on the PDP, brag about a 1.4s LCP, ship it, and move on. Then a quarter later they wonder why category browse-to-PDP rates are flat and bounce on /collections/* is climbing. The collection page is where most Shopify stores quietly leak revenue, and it's almost always slower than the PDP people spent weeks tuning.

Why collection pages are usually the slowest route

A PDP renders one product. A collection page renders 24, 36, sometimes 48 — each with an image, a price block, a swatch picker, a quick-add button, maybe a review badge. The work scales linearly with grid size, and most teams don't budget for that.

A few patterns we see on almost every audit:

  • The hero image of the collection is a 2400px banner served at full size on mobile.
  • Every product card loads a 800×800 image when it renders at 180×180 on phone.
  • Swatches lazy-fetch variant data per card, firing 36 requests on scroll.
  • A third-party filter app injects 200KB of JS before the grid paints.
  • Infinite scroll is implemented with a JS observer that re-renders the whole grid on each page.

The PDP only has to win one fight. The collection page has to win 36 small ones in parallel, and any single bad decision multiplies.

The metric nobody watches

LCP on a collection page is misleading. The largest contentful paint is often the hero banner or the first product image — both can be optimised in isolation while the rest of the grid is still janking through layout shifts and image swaps. We track three things instead:

  1. LCP on the first product card in the viewport (not the banner).
  2. CLS through to the end of first scroll, not just initial render.
  3. INP on the filter and sort controls, which is where rage-clicks happen.

If you only watch LCP, you'll ship a page that scores 95 on Lighthouse and still feels broken when a human uses it.

The image problem is bigger than you think

Shopify's image CDN is good. The Liquid image_url filter with width: parameters is good. What's not good is how most themes use them.

Here's the pattern we find in roughly 7 out of 10 themes we audit:

<img
  src="{{ product.featured_image | image_url: width: 800 }}"
  srcset="{{ product.featured_image | image_url: width: 400 }} 400w,
          {{ product.featured_image | image_url: width: 800 }} 800w,
          {{ product.featured_image | image_url: width: 1200 }} 1200w"
  sizes="(min-width: 768px) 25vw, 50vw"
  loading="lazy"
  width="800"
  height="800"
  alt="{{ product.title }}"
>

Looks fine. It isn't. On a 390px-wide iPhone, 50vw is 195 CSS pixels, which at DPR 3 is 585 device pixels. The browser picks the 800w candidate. Now multiply that by 24 cards above the fold-and-scroll region.

The fix is to be honest about the smallest reasonable size:

<img
  src="{{ product.featured_image | image_url: width: 300 }}"
  srcset="{{ product.featured_image | image_url: width: 200 }} 200w,
          {{ product.featured_image | image_url: width: 300 }} 300w,
          {{ product.featured_image | image_url: width: 450 }} 450w,
          {{ product.featured_image | image_url: width: 600 }} 600w"
  sizes="(min-width: 1024px) 22vw, (min-width: 768px) 30vw, 45vw"
  loading="lazy"
  fetchpriority="low"
  decoding="async"
  width="600"
  height="600"
  alt="{{ product.title | escape }}"
>

Three changes matter:

  • The smallest candidate is small enough to actually be picked on a phone.
  • fetchpriority="low" on below-the-fold cards tells the browser to deprioritise them behind the first row.
  • The first row of cards should get loading="eager" and fetchpriority="high" — not lazy. Lazy-loading above-the-fold images is one of the most common own-goals we see.

When to use eager on collection pages

A simple rule: the first row on mobile (usually 2 cards), the first two rows on desktop (usually 6–8 cards). Everything else is lazy. You can do this in Liquid with forloop.index:

{% assign eager_limit = 2 %}
{% if forloop.index <= eager_limit %}
  {% assign loading_strategy = 'eager' %}
  {% assign fetch_priority = 'high' %}
{% else %}
  {% assign loading_strategy = 'lazy' %}
  {% assign fetch_priority = 'low' %}
{% endif %}

In our experience this alone tends to shave 300–700ms off LCP on mid-range Android.

Filters are where the JS budget dies

Shopify's native filtering via the Storefront Filtering API is fast on the server. The slow part is what theme developers wrap around it. We've seen filter components ship with their own React runtime, then a state library, then an analytics wrapper, then a debounce util — all to manage 8 checkboxes.

A few principles that work:

  • Render the initial filter state server-side. The URL has the truth (?filter.v.price.gte=50). Read it in Liquid, render the checked states. Don't make the client do a round-trip on load.
  • Use the platform. A <form> with method="get" and checkboxes whose name matches the filter param works without a single line of JS. Progressive enhancement layers on top.
  • Debounce, don't throttle. Users tick three filters in quick succession. Fire one request after 250ms of silence, not three.
  • Replace, don't append. When filters change, swap the grid via fetch + replaceChildren, not a full page reload. But keep the URL in sync with history.pushState so back-button works.

If you're using a third-party filter app, audit its JS payload before launch. We've seen filter apps add 180KB gzipped of vendor code that runs before the grid is interactive. That's usually a deal-breaker.

Pagination beats infinite scroll on category pages

This one is contentious. Infinite scroll feels modern. It also:

  • Breaks the footer (users can never reach it).
  • Wrecks back-button behaviour unless you implement scroll restoration carefully.
  • Inflates DOM size, which inflates memory, which inflates INP on low-end devices.
  • Makes "share this collection at this product" impossible.

Classic pagination with a sensible page size (24–36) plus a "Load more" button on mobile is usually the better default. It also gives you cleaner analytics: you can actually measure how deep people browse.

The hybrid pattern

What we ship most often: pagination as the underlying mechanism, with a "Load more" button that fetches the next page and appends it, while updating the URL. Page 4 is ?page=4. Refresh works. Back works. Share works. SEO sees paginated URLs. Users get the smooth-feel UX.

Quick-add buttons: useful, expensive

Quick-add (adding to cart from the grid without visiting the PDP) is a real conversion lever. It's also where collection pages get heavy. Each card needs variant data, inventory state, and a way to open a variant picker if there are options.

The expensive way: fetch variant JSON per card on render.

The sensible way: include the minimum variant data in the product card Liquid render, and only fetch detailed data when the user actually clicks quick-add. A button with a data-product-handle attribute and a single shared modal handler is cheaper than 36 ready-to-go pickers sitting in the DOM.

For products with a single variant (no options), quick-add can be a direct POST to /cart/add.js — no modal, no fetch, no extra JS beyond the click handler.

Measuring the right things

Lighthouse will lie to you on collection pages. It runs on a clean profile with no apps installed in the storefront's third-party tag soup, no cookie consent banner timing weirdness, no logged-in user. Use it for trends, not absolutes.

What actually moves the needle is field data:

  • Chrome UX Report (CrUX) for your actual users, segmented by device.
  • RUM via Shopify's Web Performance dashboard or your own beacon.
  • Session replays on slow sessions specifically (filter by LCP > 2.5s).

When you see your p75 mobile LCP on /collections/* and it's a different number than your PDP, you'll know where to spend the next sprint.

Where we'd start

If you've got one afternoon: open your top collection page on a throttled mobile profile, count the image requests above the fold, and check what width each one is actually downloading. Nine times out of ten, that single audit will surface 60% of the wins.

If you've got a sprint: rework the product card component end-to-end — image sizing, eager loading for the first row, server-rendered filter state, and a real quick-add flow. Then put a RUM beacon on the collection route specifically so you can prove the win.

If you want a second pair of eyes on a slow storefront, our e-commerce engineering team does this work as fixed-scope audits. Or browse the rest of our storefront performance writing for more tactical breakdowns.

#Shopify#Performance#CRO#E-commerce#Frontend

Want a team like ours?

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

Start a project