Device pixel ratio (DPR) is the number of physical screen pixels, or image pixels in a screenshot, that a browser uses to draw one CSS pixel; a page reads it as window.devicePixelRatio.
At DPR 1, a 1280 x 800 viewport becomes a 1280 x 800 image. At DPR 2, the same viewport becomes 2560 x 1600: the layout is identical, but every edge has twice the detail. Automation tools call the setting deviceScaleFactor. RenderScreenshot calls it scale. This entry is part of the screenshot and rendering glossary.
A plain explanation
CSS pixels are a unit for layout. A button that is 100 CSS pixels wide is 100 CSS pixels wide on every screen. A high-density ("Retina") screen draws that button with 200 or 300 device pixels, so text and icons look sharp. A screenshot works the same way: the DPR decides how many image pixels the browser produces for the same layout.
DPR 1 and DPR 2 side by side
deviceScaleFactor 1 and 2 on 2026-10-15, then cropped to the same CSS area and enlarged with nearest-neighbour scaling so each image pixel is visible.We measured what the extra detail costs on that page:
| Capture | Image size | PNG bytes |
|---|---|---|
| 1280 x 800 viewport, DPR 1 | 1280 x 800 | 79,353 |
| 1280 x 800 viewport, DPR 2 | 2560 x 1600 | 183,895 |
| Paragraph element, DPR 1 | 416 x 77 | 10,188 |
| Paragraph element, DPR 2 | 832 x 154 | 26,841 |
Four times the pixels gave 2.3 to 2.6 times the bytes here, because flat areas compress well. A photo-heavy page grows closer to four times.
How it works technically
The browser lays out the page in CSS pixels at the viewport size. When it rasterises, it multiplies every coordinate by the DPR. Text is drawn from vector outlines, so it gains real detail. Bitmap images gain detail only when the page offers a larger file through srcset or image-set(). Otherwise the browser scales the 1x image up and it looks soft. A higher DPR can also make the page download larger images, so it can change load time.
How to set it in Puppeteer, Playwright and RenderScreenshot
Puppeteer sets it on the viewport. The script captures the same page at DPR 1 and 2 and prints what the page sees.
import puppeteer from 'puppeteer'; import { writeFile } from 'node:fs/promises'; const browser = await puppeteer.launch(); try { const page = await browser.newPage(); for (const dpr of [1, 2]) { await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: dpr }); await page.goto('https://example.com', { waitUntil: 'load' }); const ratio = await page.evaluate(() => window.devicePixelRatio); const png = await page.screenshot({ type: 'png' }); await writeFile(`pptr-dpr${dpr}.png`, png); // Element capture of the paragraph for a close-up of the same text at both ratios. await (await page.$('p')).screenshot({ path: `pptr-p-dpr${dpr}.png` }); console.log(`devicePixelRatio=${ratio} bytes=${png.length}`); } } finally { await browser.close(); }
Playwright sets it on the browser context, not the page. The scale: 'css' screenshot option gives a 1x image from a 2x context, which is handy when you want sharp rendering logic but small files.
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'); console.log('devicePixelRatio =', await page.evaluate(() => window.devicePixelRatio)); await page.screenshot({ path: 'pw-dpr2.png' }); // scale: 'css' gives one image pixel per CSS pixel, even on a 2x context. await page.screenshot({ path: 'pw-dpr2-css.png', scale: 'css' }); } finally { await browser.close(); }
That run produced a 2560 x 1600 file and a 1280 x 800 file from the same context.
RenderScreenshot accepts scale from 1 to 3 (viewport.scale in a POST body). Device presets set it for you: device=iphone_14_pro uses 3. 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 retina.png
import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const image = await client.take(TakeOptions.url('https://example.com').width(1280).height(800).scale(2));
Tested on 2026-10-15 with Node 22.18, Puppeteer 24.43.1 (run in 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
- Doubling the viewport instead of the DPR. A 2560 x 1600 viewport is a different, wider layout, often a different breakpoint. Keep the viewport and raise the ratio.
- Retina social cards. Card slots are small. A DPR 2 OG image is four times the pixels for no visible gain, and a larger file for the crawler to fetch.
- Comparing screenshots across ratios. Visual diffs between a DPR 1 baseline and a DPR 2 capture fail on every pixel. Fix the DPR in your test config.
- Expecting sharper photos. If the page has no 2x image source, a higher DPR only enlarges the 1x bitmap.
Related terms
- Above the fold: the viewport height, not the DPR, decides what fits.
- Headless Chrome: the browser that applies the ratio in most capture tools.
- Lazy loading: a high DPR can change which image file the page requests.
For when to pick 2x and how to keep files small, read the retina screenshots guide. To compare 1x and 2x on your own pages, get a free API key.