Why compression alone does not fix product image weight
There are three separate levers on product image weight, and most teams only pull one of them.
| Lever | What it controls | Typical status |
|---|---|---|
| Format | AVIF vs WebP vs JPEG encoding efficiency | Usually handled |
| Quality | Compression level within that format | Usually handled |
| Dimensions | Which pixel size the device actually receives | Usually ignored |
Format and quality are multiplicative savings on a file you may not need at that size in the first place. Dimensions are the big one, because image weight scales with area, not width. Halving the width of a delivered image quarters its pixel count.
If you have already worked through format selection and compression settings, responsive delivery is the remaining lever — and usually the largest one left.
How srcset and sizes actually work
The two attributes answer two different questions, and mixing them up is the root of most broken implementations.
srcset is the menu: a comma-separated list of files with their intrinsic pixel widths, marked with a w descriptor. sizes is the layout hint: it tells the browser how wide the image will render at a given viewport, before the browser has parsed your CSS. The browser combines the two with the device pixel ratio and picks a file.
<img
src="/products/kettle-800.jpg"
srcset="/products/kettle-400.jpg 400w,
/products/kettle-800.jpg 800w,
/products/kettle-1200.jpg 1200w,
/products/kettle-2000.jpg 2000w"
sizes="(max-width: 768px) 100vw, 50vw"
width="2000" height="2000"
alt="Stainless steel pour-over kettle, side view">
Read the sizes value as a rule list: on viewports up to 768px the image fills the full viewport width; above that it fills half. A phone at 390px CSS width with a 2x display needs about 780 real pixels, so it takes the 800w file. A desktop at 1440px rendering at half width needs 720, and also takes 800w. The 2000w variant only ships to large or high-density screens that can use it.
A default sizes="100vw" left in place on an image that renders at 50vw makes every device request a file roughly twice as wide as needed — quadrupling the bytes. The image still looks correct, so nothing flags it. Check sizes against your actual grid, not against your template's default.
Keep src populated with a mid-sized variant as the fallback, and always set width and height attributes. Those two attributes let the browser reserve layout space before the file arrives, which is what prevents Cumulative Layout Shift as your gallery loads.
Choosing your variant ladder
The practical question is how many sizes to generate. Every extra variant multiplies storage, processing time and cache entries across your whole catalog — a 5,000-SKU store with 6 photos each is 30,000 source images before you generate anything.
Four variants is the sweet spot for most catalogs: 400, 800, 1200 and 2000 pixels wide. That ladder covers phones at 1x and 2x, tablets, standard desktop grids and zoom views, with roughly a 1.5-2x step between rungs. Steps smaller than that produce variants the browser rarely distinguishes between; steps larger than 2x mean devices routinely over-download.
The distribution matters for a second reason: it tells you which variant to optimise hardest. On a mobile-majority catalog the 800w file is your real product image. If your art direction, crop and colour decisions are being validated on a 2000px desktop preview, you are reviewing a file that under half your traffic ever sees.
Two adjustments worth making. First, thumbnails and grid tiles deserve their own narrower ladder — a category page tile that never renders above 400px does not need a 1200w entry in its srcset at all. Second, if you support pinch-to-zoom on the product detail page, keep the 2000w variant regardless of traffic share; that is exactly the moment a shopper is inspecting fabric texture or stitching, and a soft image there costs you the sale.
Generating variants without multiplying your production work
Responsive delivery only pays off if generating the ladder is automatic. If a person is exporting four sizes per photo by hand, the system will drift within a quarter — someone will skip the 400w on a rush drop, and the fallback behaviour will hide it.
Manual export
- Four exports per photo, per SKU
- Naming conventions drift between people
- Re-shoots require the whole ladder regenerated
- Missing variants fail silently
- Scales linearly with catalog size
Automated pipeline
- One master file per photo
- Derivatives generated on request or at upload
- Consistent naming by construction
- Re-shoot replaces the master only
- Catalog size is not the constraint
There are three common architectures. An image CDN generates derivatives on the fly from URL parameters, which is the lowest-effort option and handles format negotiation at the same time. Build-time generation suits smaller, stable catalogs where images ship with the site. Platform-native pipelines — Shopify's image transformation parameters being the obvious example — already do this for you, and the job is mostly making sure your theme emits a correct sizes value rather than a hardcoded URL.
Whichever you pick, the rule is that only one file per photo is authored. Everything downstream is derived. That principle also applies upstream: if your product imagery is produced or retouched through an automated pipeline — the approach AI product photography tools like Retouchable take — the master should land in your DAM at full resolution and let the delivery layer size it, rather than exporting web-sized finals that can never be re-derived upward.
Store masters at least as wide as your largest variant plus headroom — 2400-3000px for a 2000w ladder. Upscaling a web-sized final later never recovers the detail, and zoom views are where that shows first.
The LCP interaction: eager, lazy and fetchpriority
Responsive images and Largest Contentful Paint are tightly coupled, and getting the loading hints wrong wastes the work you just did.
On a product detail page, the main product image is almost always the LCP element. It should be loaded eagerly with an explicit priority hint:
<!-- Hero / main product image -->
<img srcset="..." sizes="..." loading="eager" fetchpriority="high" ...>
<!-- Gallery thumbnails, below-fold, related products -->
<img srcset="..." sizes="..." loading="lazy" ...>
| Image role | loading | fetchpriority |
|---|---|---|
| Main product image (LCP) | eager | high |
| Gallery thumbnails | lazy | auto |
| Below-fold detail shots | lazy | auto |
| Related product tiles | lazy | auto |
Applying loading="lazy" to the hero image is a guaranteed LCP regression — the browser defers the request until layout confirms the image is in view, adding a round trip to your most important asset. Blanket "lazy load everything" plugins do exactly this, which is why product pages sometimes get slower after an optimisation pass.
The sizes attribute matters here too. The browser's preload scanner acts on srcset and sizes before CSS is parsed, so an accurate sizes value means the correct file starts downloading in the first few milliseconds of the page. An inaccurate one means the browser either fetches a heavier file or, worse, fetches twice. For the broader picture on measuring this, see our guide to product image LCP and Core Web Vitals.
Verifying it works, and the five failure modes
Responsive images fail silently by design — the wrong variant still renders a correct-looking image. You have to check deliberately.
The fastest verification: open a product page in DevTools, set the network panel to show the Resource column, and throttle to a mobile viewport. Look at which file was actually requested and compare it to the rendered display size. Chrome's Elements panel shows "intrinsic" versus "rendered" dimensions on hover — if intrinsic is more than about 2x rendered, you are over-serving.
The five failure modes worth auditing across a catalog:
- Stale
sizesafter a redesign. The grid changed from three columns to four; thesizesvalue did not. Every tile now over-downloads. - Missing variants falling back to
src. If a 400w derivative failed to generate, the browser uses what is available. Payload silently rises for the devices that needed it most. - Lazy loading on the LCP image. Covered above, and the most damaging single mistake.
- CSS overriding the assumed layout. A container
max-widththatsizesdoes not account for means the browser reserves for a wider render than ever happens. - Absent width and height attributes. No reserved space, layout shift as each gallery image lands, and a CLS score that degrades on slower connections.
Run this check on your three highest-traffic templates rather than the whole site — product detail, category grid and search results will surface essentially every mistake in the codebase. And re-run it after any theme or grid change, because sizes is the one attribute that goes stale without anything breaking. A quarterly pass alongside a broader product image catalog audit is usually enough to keep it honest.