Images on every screen: srcset
A phone screen 400 pixels wide does not need a 4000-pixel photograph. It downloads every byte of one anyway, if that is what you served it, and then throws 99% of the pixels away while shrinking it to fit.
img { max-width: 100%; height: auto; }
That rule makes the image look right on a phone. It does nothing about the download. The bytes are already spent by the time CSS gets involved — that is the whole point of this lesson.
srcset offers the browser a choice
Instead of one src, list several files with their real widths:
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1600.jpg 1600w"
alt="A tea stall on Burns Road at seven in the morning"
width="800" height="533">
Each entry is a file, a space, and the file's intrinsic width with a w
after it. 400w means "this file is 400 pixels wide". It is not a media query
and not a screen size — it is a fact about the file, and the browser needs it
to decide.
src stays as a fallback for anything that does not understand srcset, which
today is almost nothing. Point it at a middling size.
Two things people get wrong immediately:
wis notpx.400pxin asrcsetis invalid and the whole descriptor is ignored.- The widths must be true. Label a 400-pixel file
1600wand the browser will confidently pick it for a large screen and render it blurry.
sizes tells it how big the image will be
Here is the part that is genuinely counter-intuitive.
The browser chooses which file to download before it has laid out the page — that is the point, because waiting for layout would defeat the early fetch. So at the moment it decides, it does not know that your image will occupy half a column. It only knows the viewport width.
Left to itself, it assumes the image fills the full viewport width. If your image is actually a 300-pixel thumbnail in a sidebar, that assumption makes it download something far too big.
sizes tells it in advance:
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1000px) 50vw,
400px"
alt="A tea stall on Burns Road at seven in the morning"
width="800" height="533">
Read it as a list of conditions, first match wins, last entry is the default:
- Viewport up to 600px → the image will be the full width of it.
- Up to 1000px → half the viewport.
- Otherwise → 400 pixels, flat.
The browser takes the matching value, multiplies by the device pixel ratio, and
picks the smallest file in srcset that is at least that wide. On a 375px
phone at 2× it needs about 750 pixels, so it takes the 800. On a wide desktop
at 1× it needs 400 and takes the 400.
Note that sizes describes what your CSS will do. Nothing enforces it — if
you change the layout later and forget to update sizes, the browser keeps
making decisions based on the old description and downloads the wrong file. The
two have to be kept in step by hand, which is the honest downside of this
feature.
The simpler form, for fixed-size images
If an image is always displayed at one size — an avatar, an icon, a logo — you do not care about viewport width. You care about screen density. There is a shorter syntax for exactly that:
<img
src="avatar.png"
srcset="avatar.png 1x, avatar@2x.png 2x, avatar@3x.png 3x"
alt="Ayesha Siddiqui"
width="48" height="48">
1x, 2x, 3x are density descriptors. A standard screen takes the first, a
retina screen the second. No sizes needed, because the display size never
changes.
Do not mix w and x descriptors in one srcset. Pick the one that matches
the question you are asking.
picture is for a different picture, not a different size
srcset handles "the same image, more or less resolution". <picture> handles
"a genuinely different image".
<picture>
<source media="(max-width: 600px)" srcset="stall-square.jpg">
<source media="(min-width: 601px)" srcset="stall-wide.jpg">
<img src="stall-wide.jpg"
alt="A tea stall on Burns Road at seven in the morning"
width="1200" height="500">
</picture>
A wide landscape crop works on a desktop and becomes a useless letterbox strip on a phone, where a square crop tight on the subject reads far better. That is art direction: a human decided the phone gets a different photograph, not a smaller one.
Structural rules for <picture>:
- The
<img>is required, and it goes last. It is not a fallback nobody sees — it is the element that actually renders.<picture>only chooses which source it uses. altgoes on the<img>, never on the<source>elements.<source>elements are read top to bottom and the first match wins, so overlapping conditions are decided by order.
The other use of <picture> is offering a modern format with a safe fallback:
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="A tea stall on Burns Road at seven in the morning"
width="800" height="533">
</picture>
A browser that understands AVIF takes it; otherwise WebP; otherwise the JPEG.
type lets it decide without downloading anything. AVIF is typically half the
size of JPEG at the same quality, so this is a real saving for the cost of
generating two extra files.
You can combine everything — media, type, srcset and sizes on the same
<source> — but a tag that does all four at once is difficult to read and
easy to get wrong. Reach for the simplest form that solves your actual problem.
Which to reach for
- Same image, different resolutions, fluid width →
srcsetwithwandsizes. - Same image, fixed display size →
srcsetwithx. - Different crop on small screens →
<picture>withmedia. - Modern format with a fallback →
<picture>withtype. - One small image that is already small → plain
src. Not everything needs this machinery.
Still do the basics
Everything from the images lesson applies unchanged, and it is easy to lose it in the new syntax:
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="A tea stall on Burns Road at seven in the morning"
width="800" height="533"
loading="lazy">
Real alt. width and height from any one of the files — they only need to
give the right aspect ratio, which is the same across all three, so the
browser can reserve the correct shape. loading="lazy" because this one is
below the fold; the hero image would not have it.
Checking it works
Devtools, Network tab, filter to Img, and reload at a narrow viewport. The file name of what was actually fetched is right there. Widen the window and reload — a different file should appear.
Two things that will confuse you while testing. The browser will not
downgrade to a smaller file if it already has a larger one cached, because it
already has something good enough; tick Disable cache. And a browser may
legitimately pick a larger file than you predicted on a slow connection or a
saver setting — srcset is a set of options you offer, not an instruction it
must obey.
That is the images run complete. Next: audio and video, where the files get considerably heavier and the browser starts refusing to play them on purpose.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes