← All writing
Craft · · 7 min

srcset and the guess

A site with `srcset` on everything that shipped the 2000px file every single time.

Images Responsive

Someone sent me a site to look at before Christmas because it was slow on mobile and they couldn’t work out why. They’d done the responsive images work. Every image had a srcset with five candidates in it, generated properly by the build, and it was all correct.

Every single one was serving the 2000px file to a phone.

The reason is one missing attribute and it’s the most common way this feature fails, so I’ve written down the whole mental model, because once the model is right the attribute is obvious and until then it’s magic that doesn’t work.

The browser chooses before it knows anything

Here’s the constraint everything else follows from.

When the browser hits an <img>, it wants to start downloading immediately. That’s the whole point of the preload scanner: it races ahead of the parser looking for things to fetch, because a request started 400ms earlier is 400ms of latency you never pay. On a slow connection that’s most of your loading time.

But at that moment the browser does not know how big the image is going to be. It hasn’t finished parsing the HTML, it may not have downloaded the CSS, and even when it has, the rendered width of that image depends on layout, which depends on everything.

So it has two choices. Wait until layout is settled and then fetch, which is correct and slow. Or guess now, and be fast.

It guesses. And the guess it makes, in the absence of any other information, is that your image is the full width of the viewport.

The browser has to start downloading before it knows how big the picture will be, so it guesses. Everything about this feature is a consequence of that.

Which is why sizes isn’t optional

srcset with w descriptors says “here are the files I have and how wide each one is in pixels.” That’s an inventory. It contains no information about how the image will be used.

sizes is the other half: how wide this image will actually be rendered, expressed in the same kind of media conditions you’d use in CSS. And if you leave it out, the default is 100vw, which on a 375px phone at 2x means the browser wants a 750px image, and picks the smallest candidate that’s at least that wide, and on the site I was looking at the next one up from 750 was 2000.

So the missing attribute wasn’t making the feature slightly worse. It was making it pick worse than a plain src would have.

card.html
<img src="/img/hero-800.jpg"
     srcset="/img/hero-400.jpg 400w,
             /img/hero-800.jpg 800w,
             /img/hero-1200.jpg 1200w,
             /img/hero-2000.jpg 2000w"
     sizes="(min-width: 64em) 640px,
            (min-width: 40em) 50vw,
            100vw"
     alt="…">
HTML

Read sizes the way the browser does: first matching condition wins, last value is the fallback. Above 64em the image sits in a fixed 640px column. Between 40em and 64em it’s half the viewport. Below that it’s full width. That’s not a description of the image, it’s a description of your layout, which is the uncomfortable part and I’ll come back to it.

When you know the size, use x

All of the above is for images whose rendered width depends on the viewport. Plenty of images don’t.

A logo that’s always 180px. An avatar that’s always 48px. An icon. For those the whole w and sizes apparatus is overkill and you want density descriptors:

avatar.html
<img src="/img/avatar.png"
     srcset="/img/avatar.png 1x, /img/avatar@2x.png 2x"
     width="48" height="48" alt="…">
HTML

That’s the whole thing. No sizes, no arithmetic, and it’s the case where responsive images are genuinely easy. I mention it because people read a long article about w and sizes and then apply it to a 48px avatar, and the browser now has a choice to make that has one correct answer.

<picture> is a different feature

These get conflated constantly and the split is clean once you’ve heard it.

srcset is for when the same image would be fine at any size and you just want the right number of pixels. The browser decides, because the browser knows things you don’t: the screen density, the window size, sometimes the connection.

<picture> with <source media> is for when the image itself should be a different picture. The wide hero crop is a landscape with a person off to the left, and at phone width the person is out of frame and it’s a picture of a field. You want a different crop, and that’s a decision you’re making, not a decision the browser can make for you.

The other legitimate use is format switching, offering WebP with a JPEG fallback, because <source type> lets the browser skip a format it can’t decode. Worth doing if your build produces both, though with WebP still being Chrome and Opera only I’ve found the wins narrower than the enthusiasm suggests.

The rule I use: if the answer to “which one should it pick” is “whichever is sharpest without wasting bytes,” that’s srcset. If the answer involves a human opinion about composition, that’s <picture>.

A copy of your layout

Here’s my actual reservation about all of this, and I don’t have a fix.

sizes is a copy of your layout, written in a different language, in a different file, that nothing validates.

Your CSS says the card column is 640px above 64em. Your sizes attribute says the same thing, in HTML, and if the designer changes that column to 720px next spring, the CSS gets updated and the sizes doesn’t. Nothing breaks. Nothing warns you. The site just quietly starts picking slightly wrong images, forever, and there is no way to notice except by auditing it deliberately.

I’ve mitigated it rather than solved it. Sizes go in the template alongside the markup, never hand-written per image. A component that has a fixed slot gets its sizes defined once in the include and passed nothing. And I’ve stopped trying to be precise: being roughly right is nearly all of the benefit, and sizes="(min-width: 40em) 50vw, 100vw" on a component whose real behavior is a bit more complicated is fine. The failure mode you care about is 100vw on a small image, not 50vw when it should have been 47vw.

Three things, if you skim

Put sizes on anything using w descriptors, because without it you’ve made things worse. Use x descriptors for anything with a fixed size and stop thinking about it. And <picture> is for a different image, not a different number of pixels.

The site from before Christmas went from 2.1 MB to about 300 KB on a phone, and the change was one attribute, applied in one template, in about ten minutes. Which felt great and also slightly insulting, given how long they’d spent on the build pipeline that generated the five candidates.

Read similar posts
6 min

srcset, revisited

The exact bug I wrote about in 2018, on a site built last year, by a competent team, with a modern pipeline. Which tells you which parts of this got fixed in eight years.

6 min

Container queries

Firefox shipped these two weeks ago, which means all three engines now have the thing everyone has been asking for since about 2013, and the headline everyone's using for it is not quite right.