Responsive Images
srcset and sizes for resolution switching, <picture> for art direction and format, loading and decoding for scheduling — and width/height or aspect-ratio always, so the browser can reserve the space.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
How do I send each user an image that is the right number of pixels, in a format their browser can decode, without the page jumping when it arrives?
Someone opens an article on a phone on a train. They want to see the picture, and they want the sentence they are reading to stay where it was when the picture loads.
Export the image at the largest size any layout needs, put it in an <img src>, add loading="lazy" everywhere because lazy loading is a performance feature, and let CSS scale it down.
A phone downloads a desktop-sized image over a mobile connection and then throws most of the pixels away in the downscale. The bytes were spent, the decode was larger, and the memory it occupies is the decoded size, not the encoded one.
- A phone downloads a desktop-sized image over a mobile connection and then throws most of the pixels away in the downscale. The bytes were spent, the decode was larger, and the memory it occupies is the decoded size, not the encoded one.
- Without
widthandheight, the image box has no height until the bytes arrive. The browser lays out the page with a zero-height box, paints text, and then reflows everything below when the intrinsic size becomes known — the classic layout shift (Visual Stability). loading="lazy"on an image that is visible on load delays the request until after layout, which is the opposite of what you want for the page's main image and directly delays its paint (Loading: Why Content Arrives Late).- A wide landscape crop scaled into a narrow column becomes a thin strip in which the subject is unrecognisable. That is not a resolution problem and no
srcsetwill fix it. - A modern format shipped as the only source fails hard in any browser that cannot decode it — a broken image icon, not a fallback (Polyfills vs Transpilation).
What is actually happening
In the browser, not in the framework.
srcsetwithwdescriptors lists candidates and states how many *pixels wide* each one is.sizestells the browser how wide the image will be *in the layout* at a given media condition. The browser multiplies the layout width by the device pixel ratio and picks a candidate (The Viewport and Device Pixels).srcsetwithxdescriptors is the simpler form for a fixed-size image: "this file is for 1x, this one for 2x". Nosizesis involved because the layout width is fixed.<picture>is a different mechanism with different semantics:<source>elements are evaluated in order, and the first whosemediaandtypeboth match wins. There is no browser judgement — you are choosing, and the<img>inside is the mandatory fallback and the element that actually renders.widthandheightattributes do not set the rendered size; CSS does. They give the browser an intrinsic aspect ratio before any bytes arrive, so it can reserve a correctly proportioned box during the first layout.aspect-ratioin CSS does the same job for backgrounds and for images whose ratio is known but whose attributes are not.loading="lazy"defers the request until the browser judges the image near the viewport.decoding="async"lets the browser decode off the critical path rather than blocking the frame.fetchprioritynudges the request ordering (Resource Hints).- The preload scanner discovers
<img srcset>while tokenizing, before the element exists in the DOM — which is why an image inserted by JavaScript starts far later than one in the HTML (The Preload Scanner).
What this makes the browser do
And which of it is avoidable.
- Fetching the chosen candidate, decoding it into a bitmap whose memory cost is roughly width × height × 4 bytes regardless of how well it compressed, and uploading that bitmap to the compositor (Compositing Layers).
- Decoding is the part people forget. A very large image can compress to a small file and still cost a large decode and a large amount of memory — a real problem on a low-end phone (Memory Leaks).
- A missing intrinsic ratio makes the browser lay out, paint, and then lay out and paint again when the image arrives. Providing the ratio removes an entire pass and the shift with it (The Cost of a Change).
- Lazy loading trades bytes for a later request. Below the fold that is a straight win; above the fold it inserts a round trip into the critical path (The Critical Rendering Path).
- Every distinct candidate is a separate cacheable resource. A
sizesvalue that changes with the layout can cause a second, different candidate to be fetched after a resize (Browser HTTP Caching).
Four problems wearing one name
"Responsive images" bundles four separate problems, and the reason people reach for the wrong tool is that the phrase hides which one they have. Two of them are the browser's decision and two of them are yours.
The distinction that matters most is the second row against the first. Resolution switching means the picture is the same and only the pixel count differs — the browser can and should choose. Art direction means the picture is genuinely different, and no algorithm can decide that a portrait crop preserves the subject better than a landscape one.
| Problem | The question | The tool | Who decides | What you supply |
|---|---|---|---|---|
| Resolution switching | How many pixels should this user download of the same picture? | <img srcset sizes> with w descriptors | The browser, using pixel ratio, layout width and sometimes conditions | The candidate list and an honest sizes |
| Art direction | Should this be a different crop, or a different picture, at this shape? | <picture> with <source media> | You — the first matching source wins, in document order | The condition and a real crop for each |
| Format negotiation | Can this browser decode a newer format? | <picture> with <source type> | You — the first supported type wins | An order ending in an <img> that always works |
| Space reservation | How tall is this box before any bytes arrive? | width/height attributes, or CSS aspect-ratio | The browser, at first layout, from the ratio | A ratio that matches the real image (Visual Stability) |
Reserve the space, then choose the bytes
If you take one line from this lesson, take the ratio. An <img> without intrinsic dimensions is laid out as a zero-height box, the text below it is painted at the wrong position, and the whole page moves when the bytes arrive. Two attributes remove an entire class of the most-complained-about bug on the web.
The second version below also demonstrates the division of labour: sizes describes the layout so the browser can choose, fetchpriority says this is the image the page is about, and decoding="async" says do not hold a frame hostage to a decode.
loading="lazy"below the fold, never on anything visible at load (Loading: Why Content Arrives Late).decoding="async"almost everywhere: it lets the browser decode off the critical path instead of blocking a frame (The Frame Budget).fetchpriority="high"on the one image the page is about;lowon decorative ones competing with it (Resource Hints).aspect-ratioin CSS where attributes cannot reach — background images,<picture>sources with different ratios, and framework components that strip attributes.- An
altthat says what the image communicates *here*, oralt=""if it communicates nothing. Never absent (Semantics Are Behaviour).
<img src="/hero-2400.jpg" alt="" loading="lazy">
/* CSS */
.hero img { width: 100%; }<img
src="/hero-1200.jpg"
srcset="/hero-600.jpg 600w, /hero-1200.jpg 1200w, /hero-2400.jpg 2400w"
sizes="(min-width: 60rem) 56rem, 100vw"
width="2400" height="1350"
alt="A crowded platform at Shinjuku station during the evening peak"
fetchpriority="high"
decoding="async">
/* CSS */
.hero img { width: 100%; height: auto; }The width and height attributes are not a rendered size — the CSS still controls that — they give the browser an aspect ratio before a single byte arrives, so it reserves a correctly proportioned box at the first layout instead of collapsing and then pushing the article down. sizes lets it pick a candidate suited to the actual layout rather than the largest one; fetchpriority="high" and the absence of loading="lazy" stop the page's main image from being deferred behind everything else. The empty alt in the first version also silently discards the picture's meaning.
When the bytes actually arrive
Discovery time dominates image loading, and discovery is decided by markup. An <img> in the initial HTML is found by the preload scanner during tokenizing, before the element is in the DOM. An image inserted by a component after hydration is found after the bundle has downloaded, parsed and executed.
The bars below are relative units and describe shape, not duration. What to read from them is that loading="lazy" moves discovery later *on purpose* — which is exactly right below the fold and exactly wrong above it.
- Preload scanner finds `<img srcset>` — Discovery happens during tokenizing, before the element is in the DOM — which is why a JavaScript-inserted image starts far later (The Preload Scanner).
- CSS parsed; layout width known —
sizesis a promise about this width. A wrong promise picked a candidate before this bar even started. - Hero image bytes — Started at discovery, not at layout. That head start is the whole reason the attribute lives in the HTML.
- Decode (off the main thread) —
decoding="async"is what keeps this off the frame the user is waiting for. - Below-fold image: not requested —
loading="lazy"deferred it. Below the fold this bar is free bytes; above the fold it is a self-inflicted delay. - Lazy image bytes — If this image had been the largest visible one, this whole bar would have been added to the page's main paint.
The lesson is in the two starting points. The hero bar starts at discovery and the lazy bar starts at scroll; every attribute in the markup is a decision about which of those a given image gets.
How to build it
Most important first.
- Always give the browser the ratio:
widthandheightattributes on every<img>, oraspect-ratioin CSS where attributes are impossible. This is the highest-value line in the lesson (Visual Stability). - Use
srcset+sizesfor resolution switching — the same picture at different pixel counts — and let the browser choose. It knows the pixel ratio, the layout width and sometimes the network; you do not. - Use
<picture>only when the *content* differs: a different crop, a different aspect ratio, or a format the browser must opt into. Reaching for<picture>to do resolution switching gives up the browser's judgement for no benefit. - Get
sizesright. It is a promise about layout, and a wrong promise makes the browser choose a wrong candidate before layout has happened. If the image is a full-width hero, say100vw; if it is a grid cell, describe the grid. - Mark the page's main image eagerly and give it priority; lazy-load everything below the fold. The default for anything you are unsure about is eager, because a late main image is far more expensive than an early thumbnail (Images and Fonts).
- Serve modern formats through
<picture>typesources with a universally decodable<img>fallback, and let a CDN handle the variant generation rather than committing twelve files per image (CDN Delivery).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
altis content, not metadata. It should say what the image communicates in this context — an emptyalt=""for decoration, a description for information, and nothing at all is never correct (Semantics Before ARIA).- Text baked into an image cannot be zoomed, translated, selected, searched or read aloud, and it blurs on high-density screens. Real text over an image is almost always the right answer (Responsive Typography).
- Art direction changes what is shown, which can change what the image *means*. If the mobile crop removes the part the alt text describes, the alt text is now wrong for that user —
<picture>shares onealt, so the alt must be true of every source. - Reserving space is an accessibility feature: a screen-magnifier user who has zoomed into a paragraph loses their place entirely when an unreserved image pushes it off screen, with no visual context to find it again (Visual Stability).
- Respect
prefers-reduced-motionfor animated formats. An autoplaying animated image is motion, and<picture>can serve a still frame to users who asked for less of it (Media Queries Beyond Width).
What can go wrong
sizesthat lies. Declaring100vwfor an image that renders in a 400px column makes the browser fetch the largest candidate on every device — a performance regression introduced by the responsive-images markup itself.- An
aspect-ratioin CSS disagreeing with the real image, so the reserved box is the wrong shape and either letterboxes or crops. The shift is avoided and the image is wrong. loading="lazy"on the largest visible element, which is the single most common way to make a page's main paint later while believing you optimised it.- A
<picture>whose<source>order puts a broadly supported format before a modern one — the first match wins, so the modern source is never reached. - Missing
alt. An image with noaltattribute at all is announced by its filename in some assistive technology; a decorative image needsalt=""explicitly (Semantics Are Behaviour). - The mitigation failing:
widthandheightset, but a CSS rule setsheight: autowithoutwidth: 100%(or vice versa), so the reserved ratio is overridden and the shift returns.
- Images arrive in any order relative to each other and to the text. Without a reserved ratio, each arrival reflows everything below it, so the page moves repeatedly while the user is reading (Visual Stability).
sizesis evaluated against a layout that may not be final — if a stylesheet or a web font arrives late and changes the layout width, the browser may already have committed to a candidate (Render-Blocking Resources).- A lazy image can enter the viewport during a fast scroll and finish loading after the user has scrolled past it, spending bytes on something never seen (Scroll and Input Latency).
- A viewport change mid-load can cause a second candidate to be requested while the first is still in flight; the browser may render whichever completes first.
- Images are a decoder attack surface: the browser is parsing untrusted binary in a sandboxed process, which is why image decoding is isolated and why an out-of-date browser is a real risk (The Multi-Process Browser).
- A user-supplied image URL is an outbound request to a host you did not choose. It leaks the viewer's IP and a referrer, and it can be used as a tracking pixel; a CSP
img-srcdirective is the browser-enforced half of this (Content Security Policy). - SVG is not an image in the security sense — it is a document that can contain script. Serving user-uploaded SVG from your own origin gives an attacker script execution on that origin (Cross-Site Scripting).
- Stripping metadata matters: user-uploaded photographs carry location and device information that becomes public the moment you serve the original file (Session Replay and the Privacy It Costs).
- "
srcsetis for retina screens." It is for pixel budgets, and layout width matters at least as much as pixel ratio. A 1x laptop with a wide layout can legitimately need more pixels than a 3x phone with a narrow one. - "
<picture>is the modernsrcset." They solve different problems.<picture>overrides the browser's judgement;srcsetinforms it. Use<picture>when the *content* must differ. - "
widthandheightfight responsive CSS." They set the intrinsic ratio, not the rendered size.width: 100%; height: autoin CSS still wins, and the reserved box is still correct. - "Lazy load everything." Lazy-loading anything visible on load makes the page slower. The attribute is a scheduling instruction, not a performance setting (Loading: Why Content Arrives Late).
- "A smaller file is always better." Decode cost and decoded memory scale with pixel dimensions, not file size, so an aggressively compressed enormous image is still an enormous image (Images: The Largest Bytes, Rarely the Largest Block).
Measuring it, and what changes in the field
- The network panel shows which candidate was actually chosen and its transferred size. If a phone-width emulation is fetching the largest candidate,
sizesis the reason (Reading a Network Waterfall). - Layout-shift attribution in the field names the element that moved and the shift it caused, and unreserved images are the most common single cause (Visual Stability).
- The largest visible element on load is usually an image; its request start time relative to the HTML tells you whether discovery or transfer is the problem (Loading: Why Content Arrives Late).
- The memory panel and the rendering overlay tell you about decode cost and image size in memory, which the network panel cannot (Debugging Memory).
- Lighthouse-style audits flag oversized and unsized images specifically, which makes this one of the few areas where a static audit is genuinely reliable (Measure Before Optimising).
- On a slow network, the candidate choice is the whole story: the difference between a 1400px and a 400px candidate is seconds, not milliseconds (Bandwidth vs Latency).
- On a low-memory device, decoded image memory can force the browser to discard the tab. A grid of large images is a memory problem before it is a bandwidth one.
- On a high-density screen, the browser wants roughly the ratio squared in pixels — which is why a 3x phone can legitimately want more pixels than a 1x laptop (The Viewport and Device Pixels).
- With a long list, lazy loading is what keeps memory bounded; without it, scrolling a thousand-item feed decodes a thousand images (List Virtualization).
- When the viewport changes after load, the browser may re-evaluate
sizesand fetch a larger candidate. That is correct behaviour and it costs bytes on a rotation.
- Responsive image markup is verbose, and generating the variants is a build or CDN pipeline someone has to own. For a small site with a handful of images it is genuinely more machinery than the problem justifies (Content-Hashed Assets).
sizescouples your markup to your layout. Change the grid and everysizesattribute describing it is now subtly wrong, and nothing will tell you (Design Systems).- Art direction multiplies the assets: each breakpoint is a real crop that a person has to make, and automated cropping produces the beheaded-subject failure everyone has seen.
- Letting the browser choose means you cannot predict which candidate a given user gets, which makes exact byte budgets and reproducible screenshots harder (Visual Regression Testing).
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe selection algorithms for
srcset/sizesand the first-match ordering of<picture>are specified in HTML and behave the same across engines; what differs is which image *formats* a given engine can decode, which is exactly what thetypeattribute exists to negotiate. - BROWSER-SPECIFICBrowsers are permitted to use their own judgement when choosing a
srcsetcandidate — some have used network conditions, cache contents or a data-saving preference as inputs — so two browsers on the same device and the same layout can legitimately pick different files, and a test asserting an exact URL will be flaky. - NETWORK-SPECIFICThe size of the win depends entirely on the connection: on a fast wired link the difference between candidates is imperceptible, while on a constrained mobile link it is the difference between a usable page and an abandoned one.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — an assertion that a specific candidate URL was fetched is inherently flaky, because candidate selection is deliberately left to browser judgement; assert the reserved ratio and the absence of shift instead.