ResponsiveGENERALBROWSER-SPECIFICNETWORK-SPECIFIC

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.

The question

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?

The user intent

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.

The obvious build

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.

Why it breaks

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.

How it breaks in a real browser
  • 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 width and height, 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 srcset will 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).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • srcset with w descriptors lists candidates and states how many *pixels wide* each one is. sizes tells 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).
  • srcset with x descriptors is the simpler form for a fixed-size image: "this file is for 1x, this one for 2x". No sizes is involved because the layout width is fixed.
  • <picture> is a different mechanism with different semantics: <source> elements are evaluated in order, and the first whose media and type both match wins. There is no browser judgement — you are choosing, and the <img> inside is the mandatory fallback and the element that actually renders.
  • width and height attributes 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-ratio in 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. fetchpriority nudges 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 sizes value 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.

ProblemThe questionThe toolWho decidesWhat you supply
Resolution switchingHow many pixels should this user download of the same picture?<img srcset sizes> with w descriptorsThe browser, using pixel ratio, layout width and sometimes conditionsThe candidate list and an honest sizes
Art directionShould this be a different crop, or a different picture, at this shape?<picture> with <source media>You — the first matching source wins, in document orderThe condition and a real crop for each
Format negotiationCan this browser decode a newer format?<picture> with <source type>You — the first supported type winsAn order ending in an <img> that always works
Space reservationHow tall is this box before any bytes arrive?width/height attributes, or CSS aspect-ratioThe browser, at first layout, from the ratioA 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; low on decorative ones competing with it (Resource Hints).
  • aspect-ratio in CSS where attributes cannot reach — background images, <picture> sources with different ratios, and framework components that strip attributes.
  • An alt that says what the image communicates *here*, or alt="" if it communicates nothing. Never absent (Semantics Are Behaviour).
The main image of an article
The obvious version
<img src="/hero-2400.jpg" alt="" loading="lazy">

/* CSS */
.hero img { width: 100%; }
The version that reserves and negotiates
<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.

Two images on one page, schematicrelative units — a shape, not a measurement
HTML streams in
Preload scanner finds `<img srcset>`
CSS parsed; layout width known
Hero image bytes
Decode (off the main thread)
Hero painted
Below-fold image: not requested
User scrolls; lazy image enters viewport
Lazy image bytes
  • 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 knownsizes is a promise about this width. A wrong promise picked a candidate before this bar even started.
  • Hero image bytesStarted 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 requestedloading="lazy" deferred it. Below the fold this bar is free bytes; above the fold it is a self-inflicted delay.
  • Lazy image bytesIf 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: width and height attributes on every <img>, or aspect-ratio in CSS where attributes are impossible. This is the highest-value line in the lesson (Visual Stability).
  • Use srcset + sizes for 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 sizes right. 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, say 100vw; 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> type sources 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.

  • alt is content, not metadata. It should say what the image communicates in this context — an empty alt="" 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 one alt, 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-motion for 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

Failure modes
  • sizes that lies. Declaring 100vw for 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-ratio in 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 no alt attribute at all is announced by its filename in some assistive technology; a decorative image needs alt="" explicitly (Semantics Are Behaviour).
  • The mitigation failing: width and height set, but a CSS rule sets height: auto without width: 100% (or vice versa), so the reserved ratio is overridden and the shift returns.
What can arrive out of order
  • 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).
  • sizes is 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.
Security
  • 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-src directive 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).
Misreads
  • "srcset is 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 modern srcset." They solve different problems. <picture> overrides the browser's judgement; srcset informs it. Use <picture> when the *content* must differ.
  • "width and height fight responsive CSS." They set the intrinsic ratio, not the rendered size. width: 100%; height: auto in 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

How you would see this
  • The network panel shows which candidate was actually chosen and its transferred size. If a phone-width emulation is fetching the largest candidate, sizes is 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).
Slow device, slow network, large data, old tab
  • 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 sizes and fetch a larger candidate. That is correct behaviour and it costs bytes on a rotation.
What this costs
  • 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).
  • sizes couples your markup to your layout. Change the grid and every sizes attribute 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/sizes and 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 the type attribute exists to negotiate.
  • BROWSER-SPECIFICBrowsers are permitted to use their own judgement when choosing a srcset candidate — 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.

API Designcompression
Domains that do not exist yet
  • 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.