AniUI Academy

Worked Scenario: Design an Image-Heavy E-Commerce PLP

A full worked answer for a product listing page: rendering strategy for SEO and freshness, image loading strategy against Core Web Vitals, and filter-driven state.

13 min read

An e-commerce product listing page (PLP) is a strong system design prompt because "image-heavy" and "needs to be fast" pull in genuinely opposite directions, and resolving that tension well is exactly what's being evaluated. This walkthrough follows the same clarify-then-propose structure as the previous scenario.

Clarifying requirements

Assume: this PLP must be indexable (product pages driving organic search traffic is a real business requirement for most e-commerce sites); prices and stock status change a handful of times a day, not continuously; users filter and sort client-side without a full page reload; and the audience includes a meaningful share of mobile traffic on average-to-slow connections, making the performance stage's concerns directly load-bearing here, not theoretical.

Rendering strategy: ISR, for a specific reason

Pure SSR would mean paying a render cost — hitting a product database, on every single request — for content that only actually changes a few times a day; that's real infrastructure cost for freshness nobody needed at that granularity. Pure SSG would mean a full rebuild every time any price or stock level changed anywhere in the catalog, which doesn't scale to a catalog of any real size. ISR fits precisely because of that gap: serve the cached page instantly, revalidate on a reasonable window (or, better, on-demand the moment a price or stock change actually happens, via a webhook from the inventory system) — SSG's cost profile, with a bounded, acceptable staleness window instead of requiring a full rebuild per change.

Image strategy: directly targeting LCP

On a product grid, a product image is very often the page's Largest Contentful Paint element, which makes image strategy not a nice-to-have here but the single most consequential performance lever on this specific page:

Above-the-fold product images

Never lazy-loaded — these are likely LCP candidates, and loading="lazy" would directly delay the metric they're responsible for. Preloaded if discoverable late.

Below-the-fold product images

Lazy-loaded deliberately (loading="lazy"), since delaying their fetch until scrolled-near doesn't cost anything the user notices.

All product images

Served responsively (srcset, modern formats like AVIF/WebP) with explicit width/height or aspect-ratio set, to reserve layout space and avoid a CLS-causing shift once each image loads.

Filter and sort state: in the URL, deliberately

Filters (size, color, price range) and sort order are a textbook case for state that belongs in the URL rather than purely local component state. The requirement isn't just "the grid updates when a filter changes" — it's that a shared link reproduces the same filtered view, a page refresh doesn't silently reset the user's filters, and the browser's back button steps through filter changes the way users expect on the web generally. Query params (?size=M&color=blue&sort=price_asc) satisfy all three directly; transient useState satisfies none of them, and while localStorage would survive a refresh, it wouldn't travel with a shared link the way a URL does.

Data fetching for filtered results: server state, not client state

Each filter/sort combination is genuinely server state (from the state management stage) — a query result owned by the backend, not something the client invented — which argues for a dedicated data-fetching layer (React Query, SWR, or equivalent) keyed by the current filter combination, rather than hand-rolling useEffect fetches. That gets automatic request deduplication (rapid filter changes don't pile up redundant in-flight requests for abandoned combinations) and sensible caching (returning to a previously-selected filter combination can serve from cache instantly rather than re-fetching).

What's explicitly out of scope

Personalized recommendations ("customers also bought") are a good candidate for the streaming-SSR pattern from the rendering stage — fast, shared content ships immediately while a slower, personalized section streams in — but are called out here as a deliberate omission rather than covered in depth, since the prompt was about the PLP's core listing and filtering behavior specifically.

What to remember

  • ISR fits a PLP precisely because it's public and SEO-sensitive but doesn't need per-request freshness — a bounded staleness window is an acceptable, deliberate trade.
  • Product images are very often the LCP element on a grid page — never lazy-load above-the-fold images, and always reserve their layout space to avoid CLS.
  • Filter and sort state belongs in the URL, not transient component state, because shareability, refresh-survival, and back-button behavior are real, stated requirements of how users expect a listing page to work.
  • Filtered results are server state and benefit from a dedicated data-fetching layer's deduplication and caching, not hand-rolled effects.

Check yourself

3 questions · pass 3/3 to unlock Worked Scenario: Design a Real-Time Collaborative Editor's Frontend

up to 50
  1. 1.A product listing page (PLP) needs to be indexed by search engines and shows prices that change (sales, stock-outs) a few times a day. Which rendering approach fits best, and why?

  2. 2.Given this PLP is described as 'image-heavy,' which Core Web Vital is the hero product image most directly responsible for, and what's a concrete mistake to avoid?

  3. 3.Users can filter this PLP by size, color and price range, with results updating without a full page reload. Where should the currently-selected filters live, and why does that choice matter?

3 left to answer