The problem
One image file has to serve a phone at 390 points wide with a 3× screen, a laptop at 1440, and a 5K monitor. Send the largest and you waste most of a phone's data on pixels it cannot show. Send the smallest and the desktop gets something blurred.
The browser is the only party that knows the screen size, the pixel density, the window width and the network. srcset gives it the choice.
srcset with widths
List the files with the real pixel width of each one, using the w descriptor. Then tell the browser how wide the image will be drawn with sizes, because it has to choose before it has done layout.
400wis a fact about the file: it is 400 pixels wide. It is not a condition.sizesis a list of media conditions with the width the image will occupy, and a final value with no condition as the default.srcstays as the fallback for anything that does not understandsrcset.
<img src="/media/hall-800.jpg"
srcset="/media/hall-400.jpg 400w,
/media/hall-800.jpg 800w,
/media/hall-1600.jpg 1600w"
sizes="(max-width: 700px) 100vw, 50vw"
alt="A corridor with a concealed door"
width="1600" height="900">
How the browser chooses
On a 390-point phone with a 3× screen, sizes resolves to 100vw = 390 CSS pixels, which needs 1170 device pixels — so it takes the 1600 file. On a 1200-pixel laptop at 1×, 50vw = 600, so it takes the 800 file.
It may also take a smaller one on a slow connection, and it will never downgrade an image it has already cached. That last rule surprises people testing in devtools: reload with the cache disabled.
If you write sizes="100vw" for an image that is actually 400 pixels wide in a column, the browser downloads the biggest file on every screen. Keep sizes honest — it must describe the CSS width, not the ambition.
srcset with densities
When the image is always drawn at one fixed size — an avatar, a logo, an icon — you do not need sizes. Use the x descriptor instead.
<img src="/media/avatar-96.png"
srcset="/media/avatar-96.png 1x,
/media/avatar-192.png 2x,
/media/avatar-288.png 3x"
alt="Ulvi Shabanbayli" width="96" height="96">
Never mix w and x descriptors in one srcset — pick the one that matches how the image is sized.
picture, for a real choice
srcset picks between different sizes of the same picture. <picture> picks between genuinely different files: another format, or a different crop.
The browser takes the first <source> whose type and media it can satisfy. The <img> at the end is both the fallback and the element that is actually styled — all your CSS, the alt and the dimensions belong on it.
<picture>
<source srcset="/media/hall.avif" type="image/avif">
<source srcset="/media/hall.webp" type="image/webp">
<img src="/media/hall.jpg" alt="A corridor with a concealed door"
width="1600" height="900">
</picture>
Art direction
On a wide screen a panorama works; on a phone the subject is a speck. Art direction means cropping differently, not just scaling — and that is the other job <picture> does.
<picture>
<source media="(max-width: 700px)"
srcset="/media/hall-square-800.jpg"
width="800" height="800">
<img src="/media/hall-wide-1600.jpg" alt="A corridor with a concealed door"
width="1600" height="900">
</picture>
When the crops have different shapes, giving each source its own dimensions lets the browser reserve the correct space for whichever one it picks.
Dark mode
<picture>
<source srcset="/media/diagram-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/media/diagram-light.svg" alt="How the frame is fixed into the opening"
width="900" height="500">
</picture>
Priority
loading="lazy"on everything below the fold. Never on the first image.fetchpriority="high"on the single most important image, usually the one at the top.<link rel="preload" as="image" imagesrcset="…" imagesizes="…">in the head when even the parser finds it too late.- Serve the image at the size you actually display it. A 4000-pixel photograph in a 600-pixel column is the most common performance problem on the web.