The problem a single file cannot solve
The previous lessons give you a width for a destination. A web page has no single destination: the same image sits in a 360-pixel column on a phone, a 700-pixel column on a laptop and a 1200-pixel column on a desktop, across device pixel ratios of 1, 2 and 3.
Export for the largest and you punish every phone. Export for the phone and the desktop is soft. The answer is to ship several files and describe them, letting the browser pick before it downloads anything.
srcset and sizes, and which does what
<img src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(max-width: 700px) 100vw, 700px"
width="1600" height="1067"
alt="Brass taps on a workbench">srcset lists the files and the real pixel width of each. The w is a fact about the file, not a request.
sizes tells the browser how wide the slot will be, in CSS pixels, at each viewport width. This is the part people leave out, and leaving it out means the browser assumes the image fills the viewport and downloads something much larger than it needs.
The browser then does the arithmetic you would have done: slot width times device pixel ratio, choose the smallest file at least that big. It may also choose a smaller one on a slow connection or when the file is already cached, which is a decision you cannot make from the server.
src stays as the fallback for anything that does not understand srcset.
The one genuine annoyance: sizes is written in CSS units but lives in the HTML, so it has to be kept in step with a stylesheet it cannot see. When a layout changes and nobody updates sizes, the wrong file is served and nothing visibly breaks. Most static site generators and frameworks now write both from one definition, which is the real fix.
Combining formats and sizes
<picture> handles format choice, srcset handles size, and they compose:
<picture>
<source type="image/avif" srcset="p-400.avif 400w, p-800.avif 800w, p-1600.avif 1600w" sizes="...">
<source type="image/webp" srcset="p-400.webp 400w, p-800.webp 800w, p-1600.webp 1600w" sizes="...">
<img src="p-800.jpg" srcset="p-400.jpg 400w, p-800.jpg 800w, p-1600.jpg 1600w" sizes="..."
width="1600" height="1067" alt="...">
</picture>That is nine files for one photograph, which is why nobody writes this by hand. Generate it: a shell loop over magick, a sharp script, or whatever your site builder provides. The next module’s derivative-set lesson is the production version of this.
<picture> with media attributes also gives you art direction: a different crop for narrow screens, not merely a smaller one. A wide landscape with the subject at the left becomes an unreadable strip on a phone; a square crop centred on the subject is a different photograph and the correct one.
Doing it without generating anything
An image CDN takes one master and serves derivatives from URL parameters:
https://cdn.example.com/photo.jpg?w=800&fm=avif&q=76Cloudflare Images, imgix, Cloudinary and the image pipelines built into most hosting platforms all work this way, as does the free and self-hostable imgproxy and Thumbor. The saving is real and the cost is a dependency: every image on your site now loads through somebody else’s service, on their pricing and their uptime. For a small site, pre-generating files at build time and serving them as static files is simpler, cheaper and faster.
Lazy loading, and the one image that must not be lazy
loading="lazy" tells the browser not to fetch an image until it is near the viewport. On a long page it is the single biggest saving available.
Apply it to the image at the top of the page and you make the page measurably slower. That image is usually the Largest Contentful Paint element (the thing whose arrival the browser’s own performance metric is timing) and marking it lazy pushes its request behind others and delays the moment the page looks loaded.
The rule: everything below the fold gets loading="lazy"; the hero gets fetchpriority="high" and no lazy attribute. You can see the effect in any browser’s developer tools under Lighthouse or Performance, free, in about thirty seconds.
And keep width and height on every image. A lazy-loaded image with no reserved space makes the page jump as the reader scrolls, which is how somebody taps the wrong thing.
Today: open a page you built, throttle the network to Slow 4G in developer tools, and reload. Watch which image arrives first. If it is a thumbnail from the footer, you have found the ordering problem this lesson is about.
Every lesson here is open. Your progress is recorded once you pass the Design, before Any Software exam.
Check your understanding
A developer adds a three-width srcset to every article image but omits sizes. Phones still download the 1600-pixel file although the images sit in a 340-pixel column. Why?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Neema, your AI tutor