How to check which srcset image the browser loaded
Read img.currentSrc in the console or from a script. It holds the file the browser chose, which src and srcset do not. On the probe page below the same img element resolved to hero-400.png at 390 px, hero-1600.png at the same width with DPR 3, and hero-800.png at 1280 px.
Why check this
Run this when responsive images ship, when sizes changes, and after any layout change that alters how wide an image renders. A wrong sizes value costs bytes on every mobile load and nobody notices, because the page looks right.
The defect it prevents is a phone downloading the 1600 px hero for a 390 px slot, or a 400 px file stretched across a 1280 px banner. Both pass a visual review on a desktop monitor, and both show up in one property the Elements panel never lists.
Prerequisites
- Chrome 152 and Node 22, with
npm i puppeteer-core. The driver uses the installed Chrome. - Every figure here is one capture on one machine, taken on 2026-09-12. Candidate choice depends on the Chrome version, the device pixel ratio and the cache.
- HTMLImageElement.currentSrc is the property, and it is populated once the image starts loading.
- Save this as
srcset-server.mjs. It generates a real PNG at whatever width the URL asks for, so every candidate has a real intrinsic size. One candidate lies:banner-oops.pngis declared1600wand the file is 400 px wide. Port 8864 was free here.
// srcset-server.mjs run: node srcset-server.mjs stop: Ctrl-C
import http from 'node:http';
import { deflateSync } from 'node:zlib';
const table = Array.from({ length: 256 }, (_, n) => {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
return c >>> 0;
});
const crc32 = (b) => { let r = ~0; for (const x of b) r = table[(r ^ x) & 255] ^ (r >>> 8); return ~r >>> 0; };
const chunk = (type, data) => {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
const td = Buffer.concat([Buffer.from(type), data]);
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td));
return Buffer.concat([len, td, crc]);
};
function png(w, h, grey) {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 0;
const raw = Buffer.alloc((w + 1) * h);
for (let y = 0; y < h; y++) raw.fill(grey, y * (w + 1) + 1, (y + 1) * (w + 1));
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
}
const page = `<!doctype html><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>srcset probe</title>
<style>body{margin:0;font:16px/1.5 system-ui} img{display:block;max-width:100%;height:auto}</style>
<h1>srcset probe</h1>
<img id="hero" alt="hero"
src="/img/hero-800.png"
srcset="/img/hero-400.png 400w, /img/hero-800.png 800w, /img/hero-1600.png 1600w"
sizes="(max-width: 600px) 100vw, 50vw">
<img id="thumb" alt="thumb" width="200"
src="/img/thumb-200.png"
srcset="/img/thumb-200.png 1x, /img/thumb-400.png 2x">
<img id="banner" alt="banner"
src="/img/banner-400.png"
srcset="/img/banner-400.png 400w, /img/banner-oops.png 1600w"
sizes="100vw">
`;
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
return res.end(page);
}
const m = /^\/img\/([a-z]+)-([a-z0-9]+)\.png$/.exec(req.url);
if (m) {
const w = m[2] === 'oops' ? 400 : Number(m[2]); // the trap: declared 1600w, 400 px wide
res.writeHead(200, { 'content-type': 'image/png', 'cache-control': 'max-age=600' });
return res.end(png(w, Math.round((w * 9) / 16), (w % 200) + 40));
}
res.writeHead(404, { 'content-type': 'text/plain' });
res.end('not found');
}).listen(8864, () => console.log('srcset probe on http://localhost:8864/'));
Steps
- Step 1.
Start the probe page. Stop it with Ctrl-C at the end.
node srcset-server.mjssrcset probe on http://localhost:8864/To read one image by hand, open the page in Chrome, press F12, select the image in the Elements panel and type
$0.currentSrcin the Console. The Network panel shows the same answer as a request, and the Elements panel attribute list does not show it at all. - Step 2.
Load the page three times, each in a fresh browser profile with an empty cache, and record the choice. Save this as
srcset-loads.mjs.// srcset-loads.mjs import { launch } from 'puppeteer-core'; const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; const cases = [ { label: '390 x 844, DPR 1', width: 390, height: 844, deviceScaleFactor: 1 }, { label: '390 x 844, DPR 3', width: 390, height: 844, deviceScaleFactor: 3 }, { label: '1280 x 800, DPR 1', width: 1280, height: 800, deviceScaleFactor: 1 }, ]; for (const c of cases) { const browser = await launch({ executablePath: CHROME, headless: true }); // a new profile, so an empty cache const page = await browser.newPage(); const files = []; page.on('response', (r) => { if (r.request().resourceType() === 'image') files.push(r.url().split('/').pop()); }); await page.setViewport(c); await page.goto('http://localhost:8864/', { waitUntil: 'networkidle2' }); console.log(c.label); console.log((await page.evaluate(() => [...document.images].map((i) => ` #${i.id.padEnd(7)} currentSrc ${i.currentSrc.replace(location.origin, '').padEnd(22)} naturalWidth ${String(i.naturalWidth).padEnd(5)} layout width ${Math.round(i.getBoundingClientRect().width)}`))).join('\n')); console.log(' files fetched: ' + files.join(', ')); await browser.close(); }390 x 844, DPR 1 #hero currentSrc /img/hero-400.png naturalWidth 390 layout width 390 #thumb currentSrc /img/thumb-200.png naturalWidth 200 layout width 200 #banner currentSrc /img/banner-400.png naturalWidth 390 layout width 390 files fetched: hero-400.png, thumb-200.png, banner-400.png 390 x 844, DPR 3 #hero currentSrc /img/hero-1600.png naturalWidth 390 layout width 390 #thumb currentSrc /img/thumb-400.png naturalWidth 200 layout width 200 #banner currentSrc /img/banner-oops.png naturalWidth 97 layout width 98 files fetched: hero-1600.png, thumb-400.png, banner-oops.png 1280 x 800, DPR 1 #hero currentSrc /img/hero-800.png naturalWidth 640 layout width 640 #thumb currentSrc /img/thumb-200.png naturalWidth 200 layout width 200 #banner currentSrc /img/banner-oops.png naturalWidth 320 layout width 320 files fetched: hero-800.png, thumb-200.png, banner-oops.pngThe hero moves through all three candidates. At 390 px with DPR 1 the
sizesvalue resolves to 390 CSS px and the 400w file covers it. At DPR 3 the same slot needs 1170 device pixels, so the 1600w file wins. At 1280 px the50vwbranch asks for 640 and the 800w file wins.The banner is the interesting row. At DPR 3 it laid out 98 px wide instead of 390.
- Step 3.
Prove that a live resize is not a test. Load at 1280, shrink the viewport, reload, then reload with the cache off. Save this as
srcset-resize.mjs.// srcset-resize.mjs import { launch } from 'puppeteer-core'; const browser = await launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true }); const page = await browser.newPage(); const files = []; page.on('response', (r) => { if (r.request().resourceType() === 'image') files.push(r.url().split('/').pop()); }); const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const hero = () => page.evaluate(() => { const i = document.getElementById('hero'); return `currentSrc ${i.currentSrc.replace(location.origin, '').padEnd(20)} layout width ${Math.round(i.getBoundingClientRect().width)}`; }); const since = (n) => files.slice(n).join(', ') || '(nothing)'; await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 }); await page.goto('http://localhost:8864/', { waitUntil: 'networkidle2' }); await wait(500); console.log('load at 1280 ' + (await hero()) + ' requested: ' + since(0)); let n = files.length; await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 }); await wait(1000); console.log('resize to 390 ' + (await hero()) + ' requested: ' + since(n)); n = files.length; await page.reload({ waitUntil: 'networkidle2' }); await wait(1000); console.log('reload at 390 ' + (await hero()) + ' requested: ' + since(n)); n = files.length; await page.setCacheEnabled(false); // the same as ticking Disable cache await page.reload({ waitUntil: 'networkidle2' }); await wait(1000); console.log('cache off, reload ' + (await hero()) + ' requested: ' + since(n)); await browser.close();load at 1280 currentSrc /img/hero-800.png layout width 640 requested: hero-800.png, thumb-200.png, banner-oops.png resize to 390 currentSrc /img/hero-800.png layout width 390 requested: (nothing) reload at 390 currentSrc /img/hero-800.png layout width 390 requested: thumb-200.png, hero-800.png, banner-oops.png, hero-400.png, banner-400.png cache off, reload currentSrc /img/hero-400.png layout width 390 requested: hero-400.png, thumb-200.png, banner-400.pngLine two is the rule most people miss: a browser never swaps down to a smaller candidate it already has. Line three is stronger. The reload at 390 px did request hero-400.png, and still painted hero-800.png from the cache, so the page spent both files and reported the wrong one. Only the run with the cache disabled answered the question that was asked.
- Step 4.
Compare the chosen file against the pixels the layout needs. Save this as
srcset-truth.mjs.// srcset-truth.mjs import { launch } from 'puppeteer-core'; const browser = await launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true }); const page = await browser.newPage(); await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 3 }); await page.goto('http://localhost:8864/', { waitUntil: 'networkidle2' }); console.log('devicePixelRatio 3, viewport 390'); console.log((await page.evaluate(async () => { const out = []; for (const i of document.images) { const bmp = await createImageBitmap(await (await fetch(i.currentSrc)).blob()); const need = Math.round(i.getBoundingClientRect().width * devicePixelRatio); out.push(` #${i.id.padEnd(7)} ${i.currentSrc.split('/').pop().padEnd(16)} naturalWidth ${String(i.naturalWidth).padEnd(5)} file is ${String(bmp.width).padEnd(5)} px wide, the layout needs ${need}`); } return out; })).join('\n')); await browser.close();devicePixelRatio 3, viewport 390 #hero hero-1600.png naturalWidth 390 file is 1600 px wide, the layout needs 1170 #thumb thumb-400.png naturalWidth 200 file is 400 px wide, the layout needs 600 #banner banner-oops.png naturalWidth 97 file is 400 px wide, the layout needs 293naturalWidthnever reports the size of the file. It reports the file width divided by the density the descriptor implies, so for the hero it says 390 for a 1600 px image.createImageBitmapreads the decoded bitmap and gives the real number. The thumb has no candidate above 2x, so it serves 400 px into a slot that needs 600 and nothing in the DOM says so.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| currentSrc is the smallest candidate on a wide viewport | sizes describes a narrower slot than the layout uses | Compare sizes against the measured getBoundingClientRect().width at that width |
| currentSrc does not change after a resize | The browser kept a larger candidate it already holds | Reload with the cache off, or use a fresh profile, before recording the answer |
| naturalWidth matches the layout width exactly | The descriptor, not the file, produced that number | Read the real width with createImageBitmap, as step 4 does |
| The file is narrower than layout width times DPR | The image is upscaled on screen | Add a larger candidate, or accept it and record the decision |
| The element lays out far narrower than its container | A w descriptor larger than the file shrank the intrinsic size | Fix the descriptor to the real pixel width of the file |
Common mistakes
What to check next
- How to check if images are lazy loaded: an image below the fold is chosen at a different moment, which changes the answer.
- How to find broken images on a website: a candidate that 404s leaves
currentSrcpointing at the fallback. - How to check page size: the transferred bytes that a wrong
sizesvalue adds to every mobile load. - How to test responsive design: the width sweep to repeat this check inside.
- How to find what causes horizontal scroll on mobile: an image wider than its container is a frequent cause.
FAQ
How do I check that responsive images with srcset are working?
Load the page at a fresh profile per width, then read currentSrc for each image and compare it against the file the layout needs at that width and device pixel ratio. Step 2 does all three widths in one run.
Where does Chrome DevTools show which srcset candidate loaded?
The Network panel shows the request, and hovering the src attribute in the Elements panel shows a preview with the intrinsic size. Neither lists currentSrc. Select the image and type $0.currentSrc in the Console for a direct answer.
Why does the browser keep the large image after I resize the window?
The selection algorithm may keep a candidate that is already available, and Chrome does. That avoids a second download for a size it can already paint. It also means a resized live page reports the choice made at the old width.
Does srcset depend on the device pixel ratio?
Yes. With w descriptors the browser multiplies the resolved sizes value by the device pixel ratio. The same 390 px slot picked the 400w file at DPR 1 and the 1600w file at DPR 3.
Verified
Verified by Maks VernyChrome 152.0.7977.76node 22.23.2puppeteer-core 25.10.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate9 minpublished updated Maks Verny