DEV Community

Cover image for The sizes attribute: how the browser picks a srcset candidate
Sahadat Husain
Sahadat Husain

Posted on

The sizes attribute: how the browser picks a srcset candidate

The browser picks a srcset candidate by dividing each width descriptor by the slot width from sizes, then taking a candidate whose density covers the screen's device pixel ratio. Leave sizes out and the slot is the full viewport width. On a 1280 px laptop with a 720 px column, that one omission took the download from 58.4 KB to 170.3 KB in my test.

What does the browser compute?

Three numbers per image:

  1. The slot width. The browser walks the sizes list and takes the first media condition that matches. No match falls to the last entry. No sizes at all means 100vw, which MDN's img reference states as the default.
  2. Each candidate's density. A 1600w candidate in a 720 px slot has a density of 1600 / 720 = 2.22.
  3. The density it needs. That is the device pixel ratio: 1 on most laptops, 2 or 3 on phones.

The browser then picks a candidate with enough density. The spec leaves the final choice to the browser. MDN says the user agent selects any of the available sources at its discretion, so a browser may pick a smaller file on a slow connection or reuse a larger one it has cached. The rule below is the common case, not a promise.

Can you reproduce it in code?

Yes. Here is the pick for one sizes value, plus the real bytes of each candidate, made with sharp from a 1920 × 1280 JPEG:

import sharp from 'sharp';
const src = 'jpg-sample-photo-original.jpg';
const widths = [480, 800, 1200, 1600, 1920];
const bytes = {};
for (const w of widths) bytes[w] = (await sharp(src).resize(w).webp({ quality: 75 }).toBuffer()).length;
console.log('candidate bytes', bytes);

// sizes="(min-width: 1024px) 720px, (min-width: 640px) 50vw, 100vw"
const slot = (vw) => (vw >= 1024 ? 720 : vw >= 640 ? vw * 0.5 : vw);
// Pick the smallest candidate whose density (w / slot) covers the device pixel ratio.
const pick = (slotPx, dpr) => widths.find((w) => w / slotPx >= dpr) ?? widths.at(-1);

for (const [vw, dpr] of [[375, 2], [375, 3], [414, 3], [768, 2], [1280, 1], [1440, 2], [1920, 1]]) {
  const s = slot(vw), p = pick(s, dpr), p100 = pick(vw, dpr);
  console.log(vw, dpr, s, s * dpr, p, bytes[p], '|', p100, bytes[p100]);
}
Enter fullscreen mode Exit fullscreen mode

The output:

candidate bytes { '480': 22818, '800': 58440, '1200': 112234, '1600': 170268, '1920': 218908 }
viewport  dpr  slot  needed  picked  bytes   | no sizes (100vw): picked  bytes
375       2    375   750     800     58440   | 800    58440
375       3    375   1125    1200    112234  | 1200   112234
414       3    414   1242    1600    170268  | 1600   170268
768       2    384   768     800     58440   | 1600   170268
1280      1    720   720     800     58440   | 1600   170268
1440      2    720   1440    1600    170268  | 1920   218908
1920      1    720   720     800     58440   | 1920   218908
Enter fullscreen mode Exit fullscreen mode

Table of the width the browser downloads at seven viewports, with and without the sizes attribute

Where does a missing sizes cost you?

On phones, nowhere. The image fills the screen, so 100vw is the truth, and both columns match.

On anything wider, the image sits in a column, and 100vw overstates the slot. These are the rows that changed:

Viewport DPR With sizes Without Extra bytes
768 px 2 58.4 KB 170.3 KB 111.8 KB
1280 px 1 58.4 KB 170.3 KB 111.8 KB
1440 px 2 170.3 KB 218.9 KB 48.6 KB
1920 px 1 58.4 KB 218.9 KB 160.5 KB

The 1920 px desktop is the worst. Its slot is 720 px, but without sizes the browser thinks it is 1920 px and fetches the largest file: 3.7 times the bytes it needed.

Note: sizes describes the layout, not the file. It must match your CSS at each breakpoint. If your column is 720 px but sizes says 50vw, a 1920 px screen computes a 960 px slot and fetches 1200w. Nothing errors. You just pay for it.

Why did the 414 px phone fetch 1600w?

A 414 px phone at 3x needs 1242 device pixels. The 1200w candidate falls 42 short, so the next one up wins: 1600w, 170.3 KB. That is a gap in the ladder, not in sizes.

The fix is a candidate near the common needs. The three phones in the table need 750, 1125 and 1242 device pixels. A 1280w step would catch the 414 px phone with 38 pixels to spare. Check the needs from your own analytics, not a stock list.

How do you write sizes for a real layout?

Read the slot from your CSS at each breakpoint, widest first:

<img
  src="photo-800.webp"
  srcset="photo-480.webp 480w, photo-800.webp 800w, photo-1200.webp 1200w,
          photo-1600.webp 1600w, photo-1920.webp 1920w"
  sizes="(min-width: 1024px) 720px, (min-width: 640px) 50vw, 100vw"
  width="1920" height="1280"
  alt="An iguana on a branch">
Enter fullscreen mode Exit fullscreen mode

Three habits help:

  • Use px for fixed columns and vw for fluid ones. calc() works too: calc(100vw - 32px) for a full-bleed image with 16 px gutters.
  • Order matters. The browser takes the first match, so put the widest condition first.
  • Keep width and height. They reserve the box before the file arrives; sizes does not.

sizes="auto" is newer. It lets the browser use the laid-out width of a lazy-loaded image. MDN lists auto as a valid value; check support on caniuse before you rely on it, and keep an explicit fallback after it.

How do you check what was picked?

In Chrome DevTools, select the <img> and read currentSrc in the console:

$0.currentSrc
Enter fullscreen mode Exit fullscreen mode

Resize the window and reload; a browser that already holds a larger candidate may keep it. To see the bytes on a live page, the page image analyzer lists each image's weight.

How I measured

  • Apple Silicon Mac (M1 Pro), 2026-10-17, Node 24.22.0, sharp 0.35.4 with libvips 8.18.6.
  • Source: jpg-sample-photo-original.jpg from the Blinkimg site, 1920 × 1280, 576,828 bytes. Candidates are WebP q75 at five widths.
  • The pick function is my model of the common rule, not browser code.

What I did not test

I did not record which candidate Chrome, Safari or Firefox fetched at each size. Cache reuse and data-saver settings can change the pick.


I build Blinkimg, an image compressor and converter with a developer API. I wrote this article with help from an AI assistant (Claude), which ran the code on my machine; every number in it comes from those runs.

Top comments (0)