Every e-commerce team hits the same wall eventually. The catalog grows, someone uploads 4000px photos straight from a DSLR, and Core Web Vitals quietly fall apart. The frustrating part is that the fix is rarely one big change. It is a handful of small decisions that most teams get 80% right and 20% badly wrong.
Here is what actually moves the needle.
The Problem Isn't That Images Are Large
It is that the same large image is being sent to everyone. A 2400px hero shot is correct for a 4K monitor and absurd for a phone on a 3G connection. The browser knows the viewport size. The server usually does not, unless you tell it.
The second problem is sequencing. On most product detail pages, the main product image is the LCP element. If it is lazy loaded, or if it sits behind three fonts and a marketing banner in the critical path, LCP will suffer regardless of how well compressed the file is.
Step 1: Serve the Right Size With srcset and sizes
This is the highest-return change and it takes about ten minutes per template.
html
src="/products/lamp-800.jpg"
srcset="/products/lamp-400.jpg 400w,
/products/lamp-800.jpg 800w,
/products/lamp-1200.jpg 1200w,
/products/lamp-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
600px"
width="1200" height="1200"
alt="Brushed brass floor lamp with linen shade"
fetchpriority="high"
decoding="async">
Two details teams get wrong here. First, sizes must reflect the rendered width, not the viewport width. A product image in a two-column grid on desktop is roughly 50vw, not 100vw. Second, fetchpriority="high" belongs on the LCP image and nowhere else. Applying it broadly just tells the browser everything is urgent, which is the same as telling it nothing.
Step 2: Pick a Format Strategy and Actually Commit
AVIF typically lands 40–60% smaller than an equivalent-quality JPEG. WebP is usually 25–35% smaller. Both are now widely supported, but "widely" is not "universally," and you still need a fallback path.
The cleanest approach is the element with a WebP or AVIF source and a JPEG fallback, allowing the browser to select the most efficient format it supports without breaking older clients. What you should not do is convert everything to AVIF and call it done. Older Safari versions and a long tail of embedded browsers will fall back to the plain src, and if that file is a 3MB JPEG, you have simply moved the problem.
Encoding time is the other consideration. AVIF compression is CPU-heavy. If you are generating variants on the fly per request, you will trade page weight for server cost and latency. Most teams are better off pre-generating a defined set of widths at upload time.
Step 3: Lazy Load Below the Fold, Never the LCP
loading="lazy" on a hero product image is one of the most common self-inflicted performance wounds. The browser cannot start fetching until layout is resolved, which pushes LCP out by hundreds of milliseconds.
The rule is straightforward:
LCP image: eager, fetchpriority="high"
Everything in the initial viewport: eager, default priority
Everything below the fold: loading="lazy"
Thumbnail strips and related-product carousels are the usual offenders. They sit just below the fold, they load immediately, and they compete with the image that actually matters. Lazy load them.
Step 4: Reserve Space So Nothing Jumps
CLS from images almost always comes down to missing dimensions. If you set width and height attributes on the , modern browsers compute the aspect ratio and reserve the box before the file arrives.
css
img {
max-width: 100%;
height: auto;
}
For responsive containers, aspect-ratio does the same job:
css
.product-media {
aspect-ratio: 4 / 3;
}
This matters more on catalogs with mixed aspect ratios. A store carrying furniture, lighting, garden tools, and décor in one catalog will have portrait, landscape, and square images side by side. Stores with that kind of spread — broad home and garden catalogs like Ouchcart, for example — tend to see the worst layout shift, because no single fixed ratio fits every product type. Normalize the output ratios at upload, or accept that you need per-category containers.
Measuring What Actually Changed
Field data beats lab data here. Lighthouse will tell you the image is large; it will not tell you that 40% of your traffic is on a connection where your 1600px variant never finishes loading.
Pull LCP from your RUM tool, segmented by device class and connection type. Then check the LCP element breakdown — Chrome DevTools will tell you whether the delay is time to first byte, resource load delay, resource load time, or element render delay. Each one points at a different fix.
Change one thing at a time. Ship srcset and sizes first, measure for a week, then move to formats. Bundling five optimizations into one deploy makes it impossible to know which one helped and which one broke something.
None of this is exotic. It is mostly a matter of not sending desktop-sized images to phones, not lazy loading the thing you most want to load, and reserving space before the file arrives. Do those three and LCP usually stops being the conversation.
Top comments (1)
The point about sizes reflecting rendered width, not viewport width, is the one most teams skip even after they've done the srcset work. I've seen sizes="100vw" left on a 50vw grid image and nobody notices because the page still 'looks fine' — it's just quietly shipping a 1600px file to a 700px slot.
One thing worth adding on the format strategy: your AVIF numbers (40-60% smaller than JPEG) are on the high end. Measured across a broader sample of real product photos it tends to land closer to 30-32% lighter at equal perceived quality, with WebP around 20-22%. Still a big win, just worth setting expectations so nobody's surprised when their catalog doesn't hit 60%.
On the encoding cost point — this is exactly why most teams end up either pre-generating variants at upload time, or offloading it entirely. If you don't want to build the pipeline yourself, plugins like ShortPixel, Imagify or EWWW Image Optimizer will do local conversion and resizing on WordPress. Optimole works as an image CDN with on-the-fly resizing, though its free tier is capped by monthly visitors rather than image count. Cloudflare Polish does WebP at the edge if you're already on a paid Cloudflare plan.
(Disclosure: I run an image CDN plugin for WordPress/WooCommerce that does the resize-per-viewport plus AVIF/WebP conversion automatically, caching the result — happy to talk details if useful, just didn't want to derail the thread with a pitch.)
Fully agree on lazy-loading the LCP image being the classic self-inflicted wound — it's such an easy default to leave on by accident when a lazy-load plugin applies loading="lazy" globally.