Serving the same 2400px image to a phone on 4G is donating three quarters of the weight. And the opposite — giving a Retina screen the 1x version — delivers blur. HTML has solved both problems for years with two attributes most people copy without understanding: srcset and sizes. Here's exactly how they work.
The problem: there is no single correct image
The same photo renders in dozens of contexts: iPhone with DPR 3, 1080p laptop, 4K monitor, tablet in portrait. The right image depends on two simultaneous variables:
- The slot size where it will be painted (not the device's: the layout's).
- Pixel density of the screen (device pixel ratio).
A 400 CSS px slot on a 2x screen needs 800 physical pixels. The same slot at 1x only 400. Always serving the bigger wastes; always the smaller degrades. srcset + sizes communicate both variables to the browser, which decides by itself.
Density: srcset with x descriptors
The simple mode lists variants multiplied by density:
<img
src="photo-800.jpg"
srcset="photo-400.jpg 1x, photo-800.jpg 2x, photo-1200.jpg 3x"
alt="Beach at sunset"
/>
The browser knows its DPR (2x on a typical phone) and downloads photo-800.jpg. Perfect when the image always occupies the same size in CSS px — avatars, icons, fixed cards.
Fluid width: srcset with w + sizes
When the image adapts to the viewport (heroes, fluid grids), w descriptors declare each file's real width in pixels:
<img
src="hero-1200.jpg"
srcset="
hero-480.jpg 480w,
hero-800.jpg 800w,
hero-1200.jpg 1200w,
hero-2000.jpg 2000w
"
sizes="(max-width: 600px) 100vw, 50vw"
alt="Article cover"
/>
And here comes sizes, the attribute almost nobody configures right. Its value is NOT a style or fixed size: it's a hint you give the browser about how much viewport width the image will occupy. The syntax:
sizes="[media query] size, [media query] size, fallback"
Reading the example: if the viewport is 600px or narrower, the image will take 100% of width; if larger, 50%. With that information plus the srcset list, the browser computes the minimum needed: viewport 1400px, DPR 2 → needs 1400 × 0.5 × 2 = 1400 pixels → downloads hero-2000.jpg (the first exceeding the calculation).
Typical sizes mistakes: omitting it entirely (the browser then assumes 100vw and oversizes everything), or writing design values instead of the slot's real widths after padding and gaps.
Art direction: picture for changing the photo, not just the size
srcset serves variants of THE SAME aspect ratio. When you want different composition —a square-cropped portrait on mobile, panoramic on desktop— <picture> enters:
<picture>
<source media="(max-width: 600px)" srcset="portrait-480.jpg" />
<source media="(max-width: 1024px)" srcset="square-800.jpg" />
<img src="panorama-1600.jpg" alt="Team working" />
</picture>
The browser evaluates conditions in order and uses the first match; the final <img> is the mandatory fallback. It's also the mechanism for switching modern formats with fallback:
<picture>
<source type="image/avif" srcset="hero.avif" />
<source type="image/webp" srcset="hero.webp" />
<img src="hero.jpg" alt="Cover" />
</picture>
Mental rule: srcset/sizes for resolution; picture for art direction and format. They can combine within one block.
The actual savings numbers
With a page whose hero occupies 50vw and mixed traffic:
| Device | Unoptimized | With srcset+sizes |
|---|---|---|
| Mobile 4G, DPR 3, 390px | 2000w (~450 KB) | 390×0.5×3 = 585 → 800w (~60 KB) |
| Laptop DPR 1, 1440px | 2000w | 720→1200w (~180 KB) |
| 4K monitor DPR 2, 2560px | 2000w | 2560×0.5×2=2560 → 2000w (~450 KB) |
Every visitor receives what's fair: the mobile drops from 450 KB to 60 KB with zero visual loss. Multiplied across a full page of images, the difference between an LCP of 2s versus 5s lives here — and LCP is the Core Web Vitals metric Google scores.
Preparing the variants
Generating 4-5 sizes per image manually is unfeasible; modern CMSes do it automatically (next/image generates and even assigns sizes). If you generate variants yourself, also compress each to its optimal weight: our image converter produces WebP/AVIF/JPG/PNG adjusting quality, and for complete batches the ZIP resizer applies the same resize to dozens of files at once. The full optimization pipeline is developed in compressing images without losing quality.
FAQ
Can the browser choose better than what I write in sizes? No: without sizes, it assumes 100vw. Describing the layout is your job; no heuristic inspects your CSS.
Should I add lazy loading too? Yes, they're complementary: loading="lazy" defers download; srcset decides which to download. Combine them except on the LCP image (which must load eager with high fetchpriority).
How many variants should I generate? 4-6 well-chosen widths cover any real combination. More variants just bloat HTML and fragment cache.
Generate your optimized variants with the online image converter, free and right in your browser.