Responsive Images Done Right

On a typical content page, images outweigh everything else combined — HTML, CSS, and JavaScript together usually total less than the hero photo. Which means image handling is the single biggest lever most site owners have on page weight, and the difference between doing it well and doing it badly is not subtle: serving a 2400-pixel-wide photograph to a 360-pixel phone screen wastes the majority of the bytes transferred, on the connection type least able to afford them.

The web platform solved this years ago with srcset, sizes, and <picture> — a trio that lets the browser choose the right file for each device. The syntax has a reputation for being confusing, and the reputation is half-deserved: the underlying model is simple, but nobody explains the model. Let's fix that, then cover what WordPress already does for you and where you still need to intervene.

The problem in one sentence

One image element must serve screens from a 320-pixel phone to a 4K monitor, at pixel densities from 1× to 3×, over connections from fiber to one bar of rural cellular — and no single file is right for all of them.

The fix is equally one-sentence: you provide a menu of files, you describe the layout, and the browser — which knows the device — picks. Everything below is just the grammar for the menu and the description.

srcset: the menu of files

The srcset attribute lists the same image at multiple widths, each annotated with its actual pixel width using the w unit:

<img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w, hero-2000.jpg 2000w" ...>

Two things people misread here:

  • 400w means "this file is 400 pixels wide" — a fact about the file, not an instruction about when to use it. You are not writing media queries; you are labeling inventory.
  • The plain src stays as a fallback for anything that doesn't understand srcset, and as the default. Pick a sensible middle size.

The browser combines this menu with the display size and the device pixel ratio. A 400-CSS-pixel slot on a 2× "retina" screen needs roughly 800 real pixels, so the browser grabs hero-800.jpg. Same slot on a 1× screen: hero-400.jpg. You wrote no logic; you just labeled files honestly.

How many sizes, and which?

A serviceable rule of thumb: cover your range in steps where each file is roughly 1.5–2× the previous width — e.g. 400, 800, 1200, 1600, 2000. Finer steps buy little; coarser steps force bad choices. Cap the largest at the biggest size your layout can actually display at 2× density — if your content column is 800 px wide, a 1600 px file is your ceiling and that 4000 px original should never be in the menu.

sizes: the layout description (where everyone goes wrong)

Here's the wrinkle: the browser must choose a file before it has downloaded your CSS and computed the layout — image fetching starts during HTML scanning, for speed. So it doesn't yet know whether your image will render at 100 px or 1000 px. The sizes attribute is you telling it in advance:

sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 800px"

Read as: "below 600 px viewports this image spans the full width; up to 1024 px it takes half; otherwise it renders at a fixed 800 px." The browser evaluates the conditions left to right, takes the first match, converts to pixels, multiplies by pixel density, and picks the smallest file in the menu that's big enough.

The classic failure: omitting sizes while using width descriptors makes the browser assume 100vw — full viewport width. For a thumbnail in a sidebar, that assumption is off by 5–10×, and the browser dutifully downloads a huge file for a tiny slot. If your responsive images "aren't working," an inaccurate or missing sizes is the culprit far more often than a broken srcset. You can audit this in DevTools: inspect the image, check "Current source," and compare against its rendered size.

You don't need pixel-perfect accuracy — roughly right is fine, since the menu is quantized anyway. You do need to update sizes when you redesign the layout, which is the attribute's honest maintenance cost.

picture: for when the crop must change

srcset serves the same image at different resolutions. Sometimes you need a genuinely different image per context — this is art direction, and it's what <picture> is for.

The canonical case: a wide cinematic hero shot that works beautifully on desktop becomes an unreadable ribbon on a phone, where you want a tight square crop of the subject instead. With <picture>, you write the media queries and the browser obeys:

<picture><source media="(max-width: 600px)" srcset="hero-square-400.jpg 400w, hero-square-800.jpg 800w" sizes="100vw"><img src="hero-wide-1200.jpg" srcset="hero-wide-1200.jpg 1200w, hero-wide-2000.jpg 2000w" sizes="100vw" alt="..."></picture>

The distinction worth tattooing somewhere: with srcset/sizes the browser decides (resolution switching); with picture's media attributes you decide (art direction). Use the browser's judgment for "same photo, different size" — it can factor in things you can't, like data-saver settings. Reserve your own judgment for "different photo entirely." If you find yourself using <picture> just to serve smaller versions of the same crop, you've picked the heavier tool for no gain.

<picture> has a second job: format negotiation. A <source type="image/avif"> followed by a WebP source and a JPEG fallback lets each browser take the best format it supports. In practice, though, this is better handled below the HTML...

Formats and the case for not hand-writing any of this

On formats, the short version: WebP typically lands 25–34% smaller than an equivalent JPEG (per Google) and enjoys universal support; AVIF is often smaller still, at the cost of slower encoding; use JPEG/PNG as the compatibility floor and reserve PNG for images needing sharp-edged transparency; SVG owns logos and icons.

But here's the practical truth: format negotiation and resize generation are exactly the kind of mechanical work that belongs in infrastructure, not in your hands. An image CDN or optimization service (many hosts and plugins bundle one) will generate the size menu, convert formats based on the Accept header, compress intelligently, and serve from edge locations — turning most of this article into configuration. The parts no service can do for you: writing accurate sizes for your layout, and making art-direction decisions. Those are judgment; the rest is plumbing.

What WordPress already does — and where it needs help

WordPress has generated multiple sizes on upload and emitted srcset automatically since version 4.4, and adds native lazy loading too. That covers a lot. The gaps:

  • Its sizes guesses are generic. Classic themes often emit approximations that don't match your actual layout; modern block themes do better. Spot-check your main templates in DevTools as described above.
  • It won't fix oversized originals in old content, and it caps generated sizes — check that the cap matches your layout's real maximum.
  • Upload hygiene still matters. WordPress resizes but doesn't aggressively recompress. A 12 MB camera original becomes several large derivatives. Rough rule: exporting at quality 75–85 before upload, or letting an optimization plugin recompress, typically halves file sizes with no visible loss.
  • Themes and page builders sometimes bypass the whole system for hero sections and background images — CSS background-image gets no srcset at all. If your biggest image is a CSS background, consider restructuring it as a real <img> with object-fit: cover, which restores every mechanism in this article and lets you prioritize it.

Three finishing touches that move Core Web Vitals

  1. Always set width and height attributes (or aspect-ratio in CSS). The browser reserves the space before the image loads, eliminating the layout shift that damages your CLS score. WordPress does this for content images; verify your theme does for template images.
  2. Don't lazy-load your LCP image. Lazy loading below-the-fold images is pure win; lazy-loading the hero delays your Largest Contentful Paint. Mark the hero loading="eager" and, better, fetchpriority="high" — that one attribute is among the cheapest LCP improvements available.
  3. Audit with the network tab, not your eyes. Load key pages at phone width with the device toolbar, and check what actually downloaded. The number to watch: total image bytes for a mobile view of a typical page. Under ~500 KB is comfortable; multiple megabytes means something in this article is being skipped.

The short version

srcset lists what files exist; sizes declares how big the slot is; the browser does the math. <picture> is only for changing the actual image — crop or format. WordPress handles the mechanics; you own upload hygiene, accurate sizes, and hero-image priority. Get those right and you've optimized the heaviest thing your site ships — usually in an afternoon, usually without touching a line of application code.

Photo: Engin Akyurt on Pexels