What the responsive images audit is really telling you
The audit compares the file you sent with the pixels the device needs, which is layout width times pixel ratio. Here is how to read it and what to change.
A performance report lists three images under a heading about properly sizing them, with a number of kilobytes beside each one. The images look correct on the screen in front of you, the ladder of widths is there, the srcset is filled in. The audit is not talking about your screen. It is talking about one emulated device, one layout width and one arithmetic comparison, and once you know which three numbers it uses the report stops being an opinion.
What the audit is actually comparing
For every image on the page, the tool takes three values:
- The width of the box the image is rendered in, in CSS pixels, after layout.
- The device pixel ratio of the device being emulated.
- The intrinsic width of the file that was actually downloaded.
It multiplies the first two to get the number of real pixels the device can show, compares that with the third, and converts the difference into the bytes you could have saved. That is the whole audit. It is a report about waste, not a verdict about correctness.
The worked example that confuses people most looks like this. The mobile profile uses a viewport 412 CSS pixels wide. The page has a 24 pixel gutter on each side and the article column is narrower still, so the image is laid out at 344 pixels. The emulated device pixel ratio is 1.75. The device can therefore show 344 times 1.75, about 602 pixels across. The file that arrived is 720 wide. Those 118 extra columns of pixels cannot be displayed, so they are counted as waste and the image is flagged.
Two things follow from that arithmetic. A change to the layout can flag an image that nobody touched, because adding padding narrows the column. And the phone being 412 wide is almost irrelevant: what matters is the box and the ratio.
How sizes and srcset really work
srcset with w descriptors describes the files you have. sizes describes the box the image will occupy. The browser needs the second one because it chooses the file long before it knows the layout.
<img
src="/img/case-720.jpg"
srcset="/img/case-400.jpg 400w,
/img/case-560.jpg 560w,
/img/case-720.jpg 720w,
/img/case-960.jpg 960w,
/img/case-1280.jpg 1280w"
sizes="(min-width: 960px) 600px, (min-width: 640px) 60vw, calc(100vw - 48px)"
width="1280" height="720" alt="Order flow before and after the change">The preload scanner meets this while the HTML is still parsing, before the stylesheet has been applied. It evaluates the media conditions in sizes against the viewport, gets a width in CSS pixels, multiplies it by the device pixel ratio, and takes the smallest candidate that covers the result. On our phone, calc(100vw - 48px) gives 364, times 1.75 gives 637, and the browser takes the 720 file. Close enough, and the audit will probably stay quiet.
Leave sizes out and the browser assumes 100vw. That gives 412 times 1.75, which is 721, and the 960 candidate gets downloaded for a 344 pixel wide box. The attribute is a promise the browser has no way to check, and a wrong promise is the most common reason a page with a perfectly good srcset still fails the audit.
Two related details are worth carrying around:
xdescriptors are only for images whose layout width is fixed, like a logo or an avatar. If the width depends on the viewport, usewandsizes.- If a larger candidate is already in the cache, a browser may reuse it rather than fetch a smaller one. Manual testing without clearing the cache will tell you the wrong story.
How to see it
Do not read the file names and reason about them. Ask the page, at the width the audit used, with device emulation switched on so that devicePixelRatio and clientWidth both report the emulated values:
console.table([...document.images].map((i) => ({
file: i.currentSrc.split('/').pop(),
box: i.clientWidth,
need: Math.round(i.clientWidth * devicePixelRatio),
sent: i.naturalWidth,
ratio: +(i.naturalWidth / Math.max(1, i.clientWidth * devicePixelRatio)).toFixed(2),
})));Anything with a ratio above about 1.2 is the audit's complaint in one line, and anything below 1 is worse: the browser is stretching a file that is too small. The file column tells you which candidate was chosen, which is the fastest way to find a sizes attribute that lies.
On the build side, check what widths you actually ship:
file dist/img/case-*.jpg | sed 's/.*, \([0-9]*x[0-9]*\).*/\1/'
# 400x225
# 720x405
# 1440x810A ladder like that one is the second half of the problem. Every device that needs between 721 and 1440 pixels takes the 1440 file, which is four times the pixels of the 720.
The fix
Start with sizes, because it is free and it often removes the flag on its own. Write it from the CSS rather than from memory, and keep the breakpoints in the same order and the same values as the stylesheet. If the layout is complicated, describe the common case correctly and accept a little overshoot at the edges.
Then fill in the ladder. Steps of roughly 1.3 times keep the average overshoot small:
const widths = [360, 480, 640, 840, 1080, 1360, 1680];
for (const w of widths) {
await resize(source, w).toFile(`dist/img/${name}-${w}.jpg`);
}Seven files instead of three sounds wasteful and is not: each visitor still downloads exactly one, and the build produces them in seconds. The only real cost is storage and a slightly longer srcset string, which compresses to almost nothing.
Three more things belong in the same change:
- Keep
widthandheighton the element so the box is reserved before the file arrives. Reserving space is the same discipline as matching fallback font metrics, and it removes the same category of layout shift. - Mark the image in the first viewport as high priority and never mark it as lazy. Lazy loading the largest image delays the paint the report is measuring.
- Leave the
srcattribute pointing at a middle width, so a client that ignoressrcsetstill gets something reasonable.
How to check it worked
Run the console snippet again at the same emulated width. The ratio column is the answer:
file box need sent ratio
case-640.jpg 344 602 640 1.06
case-1080.jpg 768 1344 1360 1.01Then resize the window slowly and watch currentSrc change as the breakpoints pass. If it never changes, the browser is reusing a cached candidate or your sizes has no media conditions that match. Re-run the audit on the same profile, with an empty cache, and compare the reported savings rather than the score.
What to watch out for
- The audit is wrong for art made of hard edges. Halftone dots, line drawings, codes, thin rules and screenshots with text in them fall apart when a browser scales them, in a way a slightly soft photograph does not. This is the same reason some images are still PNG on a site where everything else is modern.
- Do not answer the flag by shipping a file smaller than the device needs. A blurry hero image is a worse outcome than 30 kilobytes, and the same report will not tell you about it.
sizesand the CSS drift apart. A redesign that narrows a column leaves the old attribute in place and every visitor pays for it. Generate the attribute from the same tokens as the layout if you can.- One emulated device is one data point. A hero judged on the phone profile can be flagged while being exactly right on the desktop where most of that page's readers are.
- Devices with a pixel ratio of three exist, and capping the ladder means they upscale. That is usually the right trade for photographs and the wrong one for anything with text in it.
The audit is arithmetic, and arithmetic is easy to argue with once you can see all three inputs. Most of the time the honest fix is a sizes attribute that tells the truth and two more files in the ladder, which takes an afternoon and never needs revisiting. The rest of the time the tool is measuring something it cannot judge, and the right answer is to write down why the file is that size and move on. Reports are useful exactly as far as you understand what they compare, which is also why an animation can ruin a paint metric while the page in front of you looks perfect.
Questions and answers
- Why is a 720 pixel image flagged on a phone that is only 412 pixels wide?
- Because the comparison is not against the screen width. The tool takes the width of the box the image is rendered in, multiplies it by the device pixel ratio of the emulated device, and compares the result with the intrinsic width of the file. An image inside a 344 pixel column on a device with a ratio of 1.75 needs about 602 pixels, so a 720 wide file carries pixels nobody will see.
- What does the sizes attribute actually do?
- It tells the browser how wide the image will be laid out, before the layout exists. The preload scanner reads it while the HTML is still parsing, multiplies the value by the device pixel ratio and picks the smallest candidate in srcset that covers it. If sizes is missing, the browser assumes the full viewport width, which on a phone usually means downloading the largest file in the list.
- Should I add more widths or just one smaller file?
- More widths. A ladder that jumps from 720 to 1440 forces every device needing 800 pixels to take 1440 and pay for four times the pixels. Steps of roughly 1.3 times keep the overshoot small, and the extra files cost nothing at serve time because each visitor still downloads exactly one.
- When should I ignore the responsive images audit?
- When the image is made of hard edges rather than photographic detail. Dot patterns, line art, codes, diagrams with thin rules and screenshots containing text all break down when the browser scales them, and the artefacts are visible where a slightly soft photo would not be. Keep the larger file, note why, and spend the savings somewhere else.