skip to content

A product page ships images as <img srcset="…400w … 2000w" sizes="100vw">, but CSS renders each one inside a 480px-wide column on desktop. What is going wrong, how would you confirm which candidate the browser actually chose, and how do you fix sizes so it does not drift again?

level: seniorimportance: should knowfreq 44%

answer

  1. invisible bug, only bytes reveal it
  2. 100vw is also the silent default
  3. area, so the error squares
  4. currentSrc tells you the truth
  5. two copies of one layout fact

basics

~20 s

The sizes value promises a full-viewport image, so the browser requests a candidate sized for the whole screen instead of the 480px column — often four times the needed bytes. Confirm via the element's currentSrc and the network panel, then rewrite sizes to match the real column width.

solid answer

~50 s

`sizes="100vw"` tells the browser the image will fill the viewport, so on a 1600-pixel desktop it resolves 1600 CSS pixels — 3200 device pixels at 2x — and takes the `2000w` file for a box that is only 480 wide. Nothing looks broken; the page just costs several times what it should, on every image, silently. Confirm rather than assume: read `currentSrc` on a few elements to see the URL actually chosen, check the network panel for the transferred size, and compare the chosen file's width with the element's rendered width times the device pixel ratio. The fix is an honest `sizes` that mirrors the layout, for example `sizes="(min-width: 1024px) 480px, (min-width: 640px) 50vw, 100vw"`. To stop the drift, stop hand-writing it: emit `sizes` from the component that owns the layout slot, derive the lengths from the same breakpoint tokens the CSS uses, and add a check that flags images whose chosen candidate is far wider than their rendered box.

code

html · 5 lines
html
<img src="item-800.jpg"
     srcset="item-400.jpg 400w, item-800.jpg 800w, item-1200.jpg 1200w, item-2000.jpg 2000w"
     sizes="(min-width: 1024px) 480px, (min-width: 640px) calc(50vw - 2rem), 100vw"
     width="2000" height="1333"
     alt="Walnut dining chair, three-quarter view">

go deeper

for a junior

Know that sizes="100vw" claims the image fills the screen, and that if the CSS renders it in a narrow column the browser downloads far more image than it can show.

for a middle

Explain the arithmetic that turns the wrong promise into wasted bytes, and write a corrected sizes list whose breakpoints match the stylesheet's.

for a senior

Show the diagnosis discipline: measure currentSrc against the rendered box and density across viewports, confirm on a reload rather than a resize, and fix the duplication rather than the one attribute.

for a principal

Own the systemic answer — who owns sizes, how it is generated from layout tokens, and what automated check keeps a redesign from silently regressing every image on the site.

## What the symptom actually is This is the characteristic responsive-image bug: **it is invisible**. The image renders crisply, nothing errors, no test fails. The only evidence is bytes on the wire. `sizes="100vw"` is also the value you get by *omitting* `sizes` entirely, so the bug arrives both by writing the wrong thing and by writing nothing. Work the numbers for a 1600-pixel-wide desktop at 2x density: ```text declared: 100vw -> 1600 CSS px -> 3200 device px -> picks the 2000w file reality: 480 CSS px -> 960 device px -> the 1000w-ish file would do ``` Because file weight scales with *area*, a 2x error in width is roughly a 4x error in bytes. Multiply by every image on a listing page and the page weight is dominated by pixels that are thrown away at paint time. ## Confirming it, in order Do not fix on suspicion; the same symptom can come from a missing candidate ladder or from CSS you have misread. 1. **Read the chosen URL.** `document.querySelectorAll('img')` and inspect `currentSrc` on each — it holds the candidate the browser settled on, whereas `src` is only what you authored. ```js [...document.images].map(img => ({ chosen: img.currentSrc, fileWidth: img.naturalWidth, boxWidth: img.getBoundingClientRect().width, dpr: devicePixelRatio })); ``` 2. **Compute the waste ratio.** `naturalWidth / (boxWidth * devicePixelRatio)`. Around 1 is healthy. 3 or 4 means the image is carrying several times the pixels it can show. 3. **Check the transferred bytes** in the network panel to confirm the large candidate really was fetched and not served from a warm cache from an earlier, wider session. 4. **Repeat at each breakpoint and at more than one DPR.** Emulating device pixel ratio matters: the desktop 1x check alone hides half the matrix. Reload each time — do not just resize, because a candidate already downloaded is generally kept rather than downgraded. ## The fix Write `sizes` so it mirrors the CSS that governs the slot: ```html <img src="item-800.jpg" srcset="item-400.jpg 400w, item-800.jpg 800w, item-1200.jpg 1200w, item-2000.jpg 2000w" sizes="(min-width: 1024px) 480px, (min-width: 640px) 50vw, 100vw" width="2000" height="1333" alt="Walnut dining chair, three-quarter view"> ``` Three points of craft: - **Mirror the breakpoints, do not invent new ones.** If the CSS switches at 1024, `sizes` switches at 1024. A `sizes` ladder with its own breakpoints will be wrong between them. - **Account for gutters and padding** with `calc()` — `calc(50vw - 2rem)` — rather than rounding optimistically. Under-stating makes the image soft, which is a visible defect; over-stating merely costs bytes. Err upward when unsure. - **Prune the ladder to what the layout can ask for.** With a hard 480-pixel desktop column, a `2000w` candidate is only ever reachable via a 4x screen; decide deliberately whether to keep it. ## Stopping the drift The root cause is duplication: the column width is expressed twice, in CSS and in an HTML attribute, with nothing tying them together. Any redesign silently breaks one copy. Durable answers attack the duplication: - **Own it in one component.** An image partial or component per layout slot — card, hero, article figure — emits the `sizes` string, so the value is written once per slot and not once per usage. - **Derive from the same tokens.** Generate `sizes` from the breakpoint and container-width values the stylesheet already uses, so a change moves both together. - **Automate the check.** The waste ratio above runs perfectly well in an automated page check across a few representative viewports and densities; flag any image over a threshold. Note that it should assert a *bound*, not a specific filename, because candidate selection is a hint the user agent may legitimately deviate from. - **Review it as layout, not decoration.** When a PR changes grid columns or a container max-width, `sizes` is part of that diff. ## What not to conclude Do not respond by deleting `srcset` and shipping one mid-size file for everyone; that trades a silent over-fetch for a guaranteed soft image on dense screens. And do not "prove" the fix by dragging the window narrower — the browser will keep the large candidate it already has. Reload at the target viewport, or you are measuring nothing.

  • Why is under-stating sizes worse than over-stating it, if both are wrong?
    An over-stated `sizes` costs bytes but the image still looks correct, so it degrades quietly. An under-stated one makes the browser pick a candidate with too few pixels for the box, and the user sees an upscaled, soft image on every viewport that hits it. A visible defect beats an invisible one only in detection, not in user impact, so when the exact width is uncertain, round up.
  • Could you keep sizes="100vw" and just remove the large candidates from srcset instead?
    It caps the damage but leaves the markup lying, and the cap is crude: the widest remaining candidate is now served to everyone regardless of slot width or density, so wide-column and dense-screen cases are simultaneously over- and under-served. It is a stopgap while you fix `sizes`, not a fix. The candidate ladder should express what files exist; `sizes` expresses the layout.
  • How do you test this in CI without asserting a specific filename?
    Assert a bound, not an identity. Load representative pages at a few viewport and device-pixel-ratio combinations and compute, per image, the chosen file's intrinsic width divided by its rendered width times the density; fail when that ratio exceeds a threshold. This survives the fact that candidate selection is a hint the user agent may legitimately deviate from.

saying these in an interview costs you the question

  • Concludes nothing is wrong because the image looks fine
  • Proves the fix by resizing the window instead of reloading
  • Invents new breakpoints in sizes that the CSS does not have
  • Removes srcset entirely and ships one file for everyone
  • Tests only at 1x and never emulates a dense screen

context