srcset Responsive Images and Their Effect on Captures

srcset lets the browser pick an image file by viewport and pixel density. See which file headless Chrome chose at DPR 1, 2 and 3, and how to control it.

By Simon Chiu · Updated

srcset is an attribute of <img> and <source> that lists several files of the same image at different widths or densities, so the browser can download the one that best fits the layout slot and the screen's pixel density.

For screenshots, srcset means one thing: the viewport and device pixel ratio you choose decide which image file the page loads, and so how sharp the photos in your capture are. This entry is part of the screenshot and rendering glossary.

A plain explanation

Without srcset, every visitor gets the same file: phones download a desktop-size photo, and high-density screens get a soft one. With it, the page offers a menu:

<img src="/hero-400.png"
     srcset="/hero-400.png 400w, /hero-800.png 800w, /hero-1600.png 1600w"
     sizes="(max-width: 600px) 100vw, 400px"
     width="400" height="200" alt="Product hero">

400w says "this file is 400 pixels wide". sizes says how wide the slot will be: the full viewport on narrow screens, 400 CSS pixels otherwise. The browser does the maths and picks a file.

Which file headless Chrome picked

We served that exact markup from a local server. Each candidate is a solid colour block labelled with its own width, so the capture shows which file the browser chose. We logged every image request and read img.currentSrc:

Three captures of the same image slot. At a 1280 pixel viewport and DPR 1 the slot shows a red block labelled 400w file. At DPR 2 it shows a blue block labelled 800w file. At a 390 pixel mobile viewport and DPR 3 it shows a green block labelled 1600w file.
The same <img srcset> captured with Puppeteer at three viewport and DPR settings, 2026-10-15. Each panel is scaled to the same width for comparison.
Viewport and DPR Slot width from sizes Pixels needed File requested
1280 px, DPR 1 400 400 hero-400.png
1280 px, DPR 2 400 800 hero-800.png
390 px mobile, DPR 3 390 (100vw) 1,170 hero-1600.png
Loaded at DPR 2, then set to DPR 1 without reload 400 400 still hero-800.png
Playwright context, 1280 px, DPR 2 400 800 hero-800.png

Each case requested exactly one file. The last Puppeteer row is the trap: once Chrome holds a larger candidate, it keeps it, so a capture loop that changes the scale between shots on the same page does not get the file a fresh visitor would get.

How it works technically

For w descriptors, the browser takes the first sizes condition that matches, resolves it to CSS pixels, multiplies by window.devicePixelRatio and picks a candidate at least that wide. Browsers are allowed to choose differently, for example a smaller file on a slow connection, or a cached larger one, as the reload test showed. For x descriptors ([email protected] 2x) the browser matches the density directly. <picture> with <source media> is different: there the page decides, by media query, and the browser must obey.

How to control it in Puppeteer, Playwright and RenderScreenshot

Set the viewport and device pixel ratio before the page loads; that is all srcset reads.

Puppeteer:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 2 }); // before goto
  await page.goto('https://example.com', { waitUntil: 'load' });
  const chosen = await page.$$eval('img', (imgs) => imgs.map((i) => i.currentSrc));
  console.log(chosen); // the files the browser actually picked
  await page.screenshot({ path: 'dpr2.png' });
} finally {
  await browser.close();
}

Playwright. The ratio belongs to the context, so a new context gives a clean image cache too:

import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const context = await browser.newContext({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2 });
  const page = await context.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'dpr2.png' });
} finally {
  await browser.close();
}

RenderScreenshot. Set scale (1 to 3) in the request, or a device, which sets the viewport and scale together. Send one request per scale rather than reusing one page. See viewport options.

curl --get https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  --data-urlencode 'url=https://example.com' \
  -d width=1280 -d height=800 -d scale=2 \
  --output dpr2.png
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);
const phone = await client.take(TakeOptions.url('https://example.com').device('iphone_14'));

The measurement and both browser scripts were tested on 2026-10-15 with Node 22.18, Puppeteer 24.43.1 (headless shell mode, Chrome 148) and Playwright 1.64.1 (Chromium 156). The RenderScreenshot snippets follow the API docs and were not run for this page.

Common mistakes

  • Raising the scale for sharper photos when the page has no larger candidate. The browser can only enlarge the biggest file it is offered.
  • Changing the scale after load. As measured, the browser keeps the larger file it already has. Navigate again in a fresh page or context.
  • Forgetting lazy images. loading="lazy" images below the fold pick their candidate only when they come near the viewport. See lazy loading.
  • Paying for DPR 3 thumbnails. A 1,600-pixel photo in a 400-pixel thumbnail is wasted bytes. The retina screenshots guide covers when 2x is worth it.

To see which files your own pages serve at 1x and 2x, get a free API key and capture them at both scales.

Frequently asked questions

Why is the image in my screenshot blurry when the site has a high-resolution version?
The browser picked a smaller srcset candidate because the capture ran at device pixel ratio 1, or because the slot is small in the sizes attribute. Raise the device scale factor and reload; the browser then requests a larger file.
Does srcset choose the image by viewport width or by screen density?
Both. With w descriptors the browser multiplies the slot width from sizes by the device pixel ratio and picks the smallest candidate that covers it. With x descriptors it matches the device pixel ratio directly.
Do I need to reload after changing the device scale factor?
Yes. In our test Chrome kept the 800w file after we lowered the ratio from 2 to 1 on a loaded page. Set the viewport and scale before navigation.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.