Your product page's largest element arrives last on mobile
In short
A product page that is slow on a mid-range phone is usually an image-delivery fault, not a general performance problem. Capture it on a throttled device, identify which element the browser reports as largest, and the cause narrows to 1 of 4: the image is discovered late behind a script, negotiated badly, withheld by the gallery, or not the largest element at all.
Key takeaways
- Identify the reported largest element before optimising anything. It is often a promo bar, a banner or a review widget.
- Discovery order beats file size: an image URL that only exists inside JavaScript cannot be requested until the bundle runs.
- loading=lazy on the largest above-the-fold image is a deprioritisation you did to yourself.
- A wrong sizes attribute makes the browser pick a candidate 2 to 4 times larger than the slot it renders into.
- A preload that does not exactly match the srcset and sizes the img ends up using downloads the image twice.
- Set a per-breakpoint byte budget and enforce it in CI, rather than trusting a CDN default nobody has read.
Before touching the image pipeline, find out what the browser thinks the largest element is. On a throttled mid-range phone, roughly half of product pages report something other than the product shot: a promotional bar, a cookie banner, a lifestyle banner above the gallery, or a block of review text. Optimising the product image on those pages moves nothing, and teams spend weeks there.
The measurement takes minutes. Load the page on a real device or an emulated one at a 390 CSS pixel viewport with network and CPU throttling, and read the reported largest-contentful-paint entry — DevTools shows it, and a PerformanceObserver watching that entry type reports the element in production. Do it 3 times and take the worst, because a warm cache hides the problem you are looking for.
Confirm the element before you optimise anything
There are only 2 useful outcomes of that first check, and they lead to different pages of work. If the reported element is not the product image, the fix is a layout or content decision — the thing above the product shot is too big, and no image work will help until it is smaller or gone. If it is the product image, you have an image-delivery problem, and the next question is which of 3 stages lost the time.
One thing to rule out first: whether the whole storefront got slower rather than this page. If every page regressed at once and nobody deployed, that is a different investigation entirely — the storefront got slower and nobody shipped code.
The four causes, and the signature that names each
| Signature | Cause | Where the fix goes |
|---|---|---|
| The image request starts only after the main bundle finishes parsing | Discovery: the URL exists only inside JavaScript | Markup and rendering — get an img into the initial HTML |
| Transfer size is large and identical on desktop and phone | Negotiation: no responsive candidates, no modern format | Image pipeline |
| The image request starts early but downloads a candidate far wider than the slot | Negotiation: a wrong sizes attribute | Markup — sizes, not the CDN |
| The download completes early and the paint happens much later | The gallery script un-hides the slide after it mounts | Script order and CSS — render the first frame without JS |
| The reported element is a banner, a bar or a text block | Layout: the product shot is not above the fold at 390 px | Content and layout, before any image work |
| The image is fetched twice, at 2 different widths | A preload whose candidate set does not match the img | Markup — align or remove the preload |
Discovery: the request that starts too late
Browsers scan the HTML for resources before the document has finished parsing, and start downloading images they find there. An image whose URL only appears after a JavaScript gallery component mounts cannot benefit from that scan: the browser has to download the bundle, parse it, execute it, resolve the component's data, and only then discover the image. On a mid-range phone over a constrained connection that sequence is easily a second of dead time before the request is even made.
- Put a real img element for the first frame in the server-rendered HTML. Everything else in the gallery can mount later; the first one cannot.
- Remove loading=lazy from that image. Lazy loading is correct for the other 8 shots in the gallery and actively harmful on the largest one, because it tells the browser to wait.
- Add fetchpriority=high to it, and only to it. Marking 4 images high priority is the same as marking none.
- Check for a redirect before the document. A geo or currency redirect adds a full round trip before any HTML arrives, and it delays every subsequent step — see shoppers landing in the wrong country's storefront.
- If you preload, match exactly. A preload whose candidate set differs from what the img resolves to downloads the image twice and makes the page slower than doing nothing.
Negotiation: bytes with no relationship to the screen
A 390 CSS pixel viewport on a 3x device needs about 1,170 device pixels of image width for a full-bleed product shot. Serving the same 2,400 pixel master used on desktop means roughly 4 times the pixel data and a much larger multiple in bytes once compression is accounted for. This is the most common finding and the easiest to fix, and it is fixed in the markup as often as in the CDN.
The subtler version is a sizes attribute that lies. If the image renders at half the viewport width but sizes says 100vw, the browser dutifully picks a candidate twice as wide as it needs — every time, on every product page, invisibly. Check the attribute against the rendered width at 2 or 3 breakpoints rather than trusting it, because nothing warns you when it is wrong.
Paint: downloaded early, shown late
The signature here is unmistakable once you look for it: the image finishes downloading well before the reported paint. The bytes arrived; something was preventing them from being displayed. Almost always this is the gallery — slides positioned by script, an opacity that starts at 0 and animates in on mount, or a carousel that measures the container before revealing anything.
The fix is to make the first frame render correctly with no JavaScript at all, then let the gallery enhance it. That means the first slide is visible in CSS, sized by an aspect ratio so it does not shift when the image lands, and the carousel takes over on mount without re-mounting the image. Everything competing for the main thread in those first seconds makes this worse, and third-party widgets are usually the heaviest of them — where an on-site assistant should live is partly this decision: an assistant on your site versus a presence in someone else's.
The bytes arrived on time. A carousel measuring its container decided the shopper should wait another 800 milliseconds to see them.
The decision tree
- Capture on a throttled 390 pixel viewport, 3 times, cold cache. Record the reported largest element.
- If it is not the product image, stop. Fix the layout above the gallery first, then re-measure — the answer will have changed.
- Compare the image request start time to the document start. A gap of more than a few hundred milliseconds means the browser did not find it in the HTML: a discovery problem.
- Compare transferred bytes to rendered CSS pixels. Wildly out of proportion means format or candidate selection: a negotiation problem. Check sizes before blaming the CDN.
- Compare download-complete to the reported paint. A meaningful gap means the image was ready and something withheld it: a script-order problem.
- Re-measure after each single change. Changing 3 things at once on a page with this many interacting layers tells you nothing about which one worked.
What this does not fix
Image delivery is the largest single lever on a product page and it is not the only one. A slow first byte, a heavy third-party script stack and a bloated client bundle all sit underneath this work, and none of them is addressed by better imagery. Neither is correctness: a page that paints in under 2 seconds and then shows a price the cart disagrees with has traded one complaint for another — the cart price does not match the page price is a different investigation with a different fix.
It is also worth being precise about the target. The widely used threshold for a good largest-contentful-paint is 2.5 seconds at the 75th percentile of real visits, but the definition and the thresholds are maintained by Google in the Core Web Vitals documentation and they have changed before — check the current text rather than quoting a number from memory, including this one. Lab captures are for diagnosis; the number that matters comes from field data.
Most of this work is unglamorous and easy to get subtly wrong — a preload that duplicates a download, a sizes attribute that quietly doubles every image on the site. If you are choosing someone to do it, the questions worth asking are in choosing a development partner. The surrounding architecture decisions sit in storefront architecture and platform choice, within software for retail and ecommerce; where the same page also has to answer machine readers, that overlaps with AI agents and automation.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
How do we know which element the browser counts as largest?
Read it from the browser rather than inferring it. DevTools reports the largest-contentful-paint element in its performance trace, and in production a PerformanceObserver watching that entry type can report it per visit. Inferring it by eye is where most of the wasted effort starts, because the visually dominant element and the measured one are frequently different.
Will switching to a modern image format fix a slow product page?
It helps only if bytes are the bottleneck, which is 1 of the 4 causes. If the image is discovered late or withheld by a gallery script, a smaller file arrives late in a smaller size and the reported paint barely moves. Run the diagnosis first: format work is cheap and worth doing regardless, but as a fix it is often aimed at the wrong stage.
Should the product image be server-rendered even on a fully client-rendered storefront?
Yes, at least the first frame. The initial HTML is the only place the browser's preload scanner can find an image before running JavaScript, so a server-rendered img for the first gallery slide is worth more than almost any other change on a client-rendered page. The rest of the gallery can stay entirely client-side.
Is a hero image on a product page worth its cost at all?
Yes — the product shot is the page's main content and shoppers will not buy from a placeholder. The goal is not fewer images but an image that is discovered immediately, sized for the device and painted without waiting on script. Removing the image is not the trade; removing the banner above it usually is.
- performance
- LCP
- product pages
- storefront architecture
The work behind this page
Builds from our portfolio that this page draws on.
AI Lease Management
AI-powered commercial real estate lease management for multi-brand operators — automates lease data extraction, obligation tracking, and portfolio intelligence.
Real EstateShortList
An AI recruiting screener that reads every application, scores candidates against the role, and hands recruiters a ranked shortlist with outreach already drafted.
HR & RecruitingRead next
- Four caches sit between your product page and the shopperBuild output, the edge, the browser and the in-page store. You can purge the first two on demand and only expire the other two — which is the rule every caching decision follows.definition
- The cart token: the one object a storefront can never serve from cacheThe cart token carries identity, not contents. Once you see it that way, cached cart fragments and merge rules that silently discard items stop being surprises.definition
- The public storefront token: what a shopper-side key can readA storefront credential is safe because of its scope, not its secrecy. It ships to the browser by design — so the only question that matters is what it can return.definition
Related across the site
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation