Lesson 25 of 26
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.
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
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.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.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