Skip to content
install

notes 4 min layout

note

Why srcset keeps picking the wrong image

A srcset with four good widths on it will still fetch the largest one on a phone, and the attribute doing that is not srcset. sizes is the half that decides, it is guessed far more often than it is measured, and its default is the worst value it can hold.

The selection happens before the page has a layout. A preload scanner reads the raw HTML while the parser is still working and the stylesheet has not necessarily arrived, so at the moment the browser chooses a file it does not know how wide the image will be drawn - and it will not go back and check.

sizes is how you tell it in advance. It is not a hint and it is not documentation: it is the width the browser uses in the arithmetic, and if it disagrees with the CSS then the CSS is the thing that loses.

Leave sizes off entirely and the browser uses 100vw. Every image on the page is then declared full-bleed, whatever it looks like - which is why a sidebar thumbnail can arrive as a 1600px file.

The arithmetic, in the order it runs #

  1. Resolve sizes. The media conditions are read left to right and the first one that matches wins - the rest are not consulted, the same rule <picture> uses.
  2. Multiply by device pixel ratio. A 400px box on a 2× screen needs an 800px file.
  3. Pick a candidate. The smallest w in the srcset that is at least that number. If none reaches it, the largest one.

Three consequences follow from that and each of them is a bug somebody has shipped. A sizes that is too large is expensive on every device, because step 3 rounds up from an inflated number. A sizes that is too small is worse than too large on a retina screen, because a blurry image is visible in a way a wasted 900kB is not. And a candidate list with no width above the computed need means the largest file, on the smallest phone.

The four ways it is written wrong #

A percentage
sizes="50%" is invalid and is discarded, which puts you back on 100vw. Percentages of what? There is no containing block yet. Use vw, px, em or a calc() of them.
The CSS width, copied
max-width: 800px on an image drawn at half the viewport is sizes="(min-width: 1600px) 800px, 50vw", not sizes="800px". The breakpoint in sizes is the viewport width at which the cap starts binding, and it is arithmetic rather than one of your design breakpoints.
Mixed descriptors
srcset="a.jpg 2x, b.jpg 800w" is one list with two grammars in it. Pick one: x descriptors ignore sizes completely and are for a fixed-size image, w descriptors are for one that responds.
Forgetting the container
An image inside a max-width: 72ch column is not 100vw on a wide screen and it is not 50vw on a narrow one. This is the case sizes models worst, and the one worth measuring rather than deriving.
a half-width image capped at 800px, written out
<img
  src="/img/hero-800.jpg"
  srcset="/img/hero-480.jpg 480w,
          /img/hero-800.jpg 800w,
          /img/hero-1200.jpg 1200w,
          /img/hero-1600.jpg 1600w"
  sizes="(min-width: 1600px) 800px, 50vw"
  width="800" height="450"
  alt="">

width and height are on it for a different reason and are not optional either: without them the box has no aspect ratio until the bytes land, and the layout shift that follows is the one thing on this page a visitor actually notices.

Why the number never looks wrong in DevTools #

Two behaviours hide it. A browser that has already downloaded the 1600px file will keep using it after a resize rather than fetching something smaller - going up is a visible improvement, going down is a visible regression, so re-selection is one-way. And a desktop screen at 2× reaches the largest candidate honestly, which means the machine you are testing on is the one machine where the bug is invisible.

The measurement that settles it is the file the page actually fetched against the box it was actually drawn in, taken on a real viewport rather than in a resized window. Those two numbers are not printed together anywhere in a browser's own tooling, and the ratio between them is the whole finding.

sizes="auto" moves the arithmetic to after layout and gets it exactly right. It requires loading="lazy" on the same element - which excludes anything above the fold, so the images that cost the most are the ones it cannot help with. Check support before it is your only sizes.

What to do about it on Monday #

  • Write sizes from the drawn width, not the intrinsic one, and keep the media condition in viewport terms.
  • Cap the candidate list at twice the largest drawn width. A 1600px file for an 800px box covers 2×; a 2400px one covers nobody.
  • Test at 1× on a narrow viewport with a cold cache. That is the only configuration where a wrong sizes is visible.
  • Check the ratio of bytes fetched to pixels drawn, per image, rather than the total page weight - one bad sizes on a repeated thumbnail outweighs everything else on the page.

The first three are worth doing by hand. The fourth is a number, it is different on every breakpoint, and it is the reason this note has a calculator attached to it.

Written by Ján Turský

Building LoupeKit and other browser tools out of Bratislava, under Apptiary.

run it here, no install

tools in this note