Images that do not cost a fortune
Images are usually most of the bytes on a page. srcset, sizes, modern formats and two attributes that stop the layout jumping.
On most pages, images are the majority of the transfer. Which means image handling is not a detail, it is the single largest lever you have on load time, larger than anything you will do to your JavaScript.
srcset lists the files you have with their intrinsic widths: small.jpg 400w, medium.jpg 800w, large.jpg 1600w. sizes tells the browser how wide the image will be laid out, which it needs to know before it has parsed the CSS, so you have to say it in the markup and why it feels redundant. The browser combines the two with the device pixel ratio and picks a candidate.
The arithmetic is worth seeing once, because after that it is obvious. sizes resolves to a CSS width, say 50vw at a 720px viewport, so 360px. At DPR 2 that needs 720 device pixels. The browser picks the smallest candidate at least that wide: the 800w file. Change any of the three inputs and the choice changes. Drive the instrument below across the viewport slider and the DPR toggle until that is predictable.
Which file the browser actually downloads
Device pixel ratio 2 means the slot needs 2 image pixels for every CSS pixel. A 2020s phone is 2x or 3x, and its viewport is narrow; the two cancel out less than people expect.
hero-400.avif
320px too narrow
hero-800.avif
picked: smallest one wide enough
hero-1200.avif
480px wider than needed
hero-2000.avif
1280px wider than needed
The arithmetic
sizes resolves to min-width: 700px matches at 720px, so 50vw = 360px, at DPR 2 that needs 720px, the smallest candidate at least 720px wide is 800w, so 180KB.
A 6-image gallery on this page
Without a sizes attribute this same gallery would be 4920KB. The attribute is doing 3840KB of work at this viewport.
The markup
<img
src="hero-800.avif"
srcset="hero-400.avif 400w,
hero-800.avif 800w,
hero-1200.avif 1200w,
hero-2000.avif 2000w"
sizes="(min-width: 700px) 50vw, 100vw"
width="2000" height="1125"
alt="" loading="lazy" decoding="async">The w descriptors describe the files. The sizes attribute describes the slot in your layout. The browser needs both before it has parsed your CSS, which is why sizes exists at all and why it is on the element rather than in a stylesheet.
Trail running shoes
Grip on wet rock, 240g, and a rock plate that does not fold.
Free returns for 30 days.
With width="2000" height="1125" on the element the browser knows the ratio before a single byte of image arrives, reserves 190px, and the Add to basket button never moves. aspect-ratio in CSS does the same job when the element is fluid: the attributes set the ratio, the CSS controls the width.
The person tapping Add to basket at the moment the image lands taps whatever moved into that position instead. That is the part users report as "the site clicked the wrong thing", and it has nothing to do with how many kilobytes you shipped.
<img src="…" width="2000" height="1125" alt="">
img { width: 100%; height: auto } /* ratio kept */sizes resolves to min-width: 700px matches at 720px, so 50vw = 360px, at DPR 2 that needs 720px, the smallest candidate at least 720px wide is 800w, so 180KB.
The most common mistake is omitting sizes entirely. The browser then assumes 100vw and downloads a file sized for the full viewport for an image that occupies a quarter of it. The gallery counter in the instrument shows what that costs across six images, and the number is large enough to be worth the two minutes it takes to fix.
Formats: AVIF is typically 30-50% smaller than JPEG at similar quality, WebP around 25-35%, and both are supported everywhere that matters now. Use <picture> with <source type> to offer them with a JPEG fallback. Encode at the sizes you actually serve rather than scaling a single large original in CSS, which downloads every pixel and then throws most of them away.
Then layout shift, which is the other half of image cost and the one that annoys people most. An image with no dimensions has zero height until it loads, so everything below it jumps down when it arrives, and if the user was reading, or about to tap something, they now are not. Setting width and height attributes gives the browser an aspect ratio to reserve space with, and it is two attributes. aspect-ratio in CSS does the same for responsive images. The second panel below lets you turn it off and watch the button you were about to press move out from under the cursor.
Lazy loading last, because it is over-applied. loading="lazy" on images below the fold is free performance. On the image at the top of the page it is actively harmful, it delays the thing the user came to see, and it usually is the Largest Contentful Paint element. The rule: never lazy-load anything in the first screenful, always lazy-load the rest.
Worth reading
- MDN: Responsive images ↗, The reference for srcset, sizes and picture, including the density-descriptor variant.
- web.dev: Responsive images ↗, The same material with the performance framing and the format comparison.
You should now be able to
- Write srcset and sizes correctly and know which candidate gets picked
- Prevent layout shift with width, height or aspect-ratio
- Choose a format and know roughly what it saves
- Decide correctly when to lazy-load and when not to
Loading…