How to Compare Website Screenshots at Different Device Pixel Ratios
Normalize screenshots to the same CSS-pixel scale before comparing them. Learn how to capture, resize, and compare images without hiding real layout changes.
To compare website screenshots captured at different device pixel ratios (DPRs), first put them in the same coordinate space. For layout comparisons, that usually means one image pixel per CSS pixel. In Playwright, explicitly set scale: 'css'; if you only have existing image files, resize the higher-resolution capture to the corresponding CSS viewport dimensions. Then confirm that the CSS viewport, page state, browser, and operating system also match.
A screenshot that is twice as wide and tall may simply represent the same CSS viewport captured at 2× DPR. Its larger raster does not by itself mean the page layout changed. Conversely, resizing images to equal dimensions without checking the CSS viewport can hide a real responsive-layout difference.
1. Understand DPR, CSS pixels, and screenshot pixels
window.devicePixelRatio is the ratio of display resolution in physical pixels to resolution in CSS pixels. A DPR of 2 means the browser may use two physical pixels for each CSS pixel. As a result, a 1200 by 800 CSS-pixel viewport can produce a 1200 by 800 raster at 1× or a 2400 by 1600 raster at 2×, depending on the screenshot method. [MDN: devicePixelRatio]
In Playwright’s page.screenshot(), scale: 'device' uses device pixels and is the default; scale: 'css' produces one screenshot pixel per CSS pixel. Make this option explicit so that behavior does not depend on an assumed default. [Playwright: page.screenshot()]
| What you want to compare | Coordinate space | Recommended approach |
|---|---|---|
| Same page layout at the same CSS viewport | CSS pixels | Capture both with scale: 'css', or normalize known 2× rasters to CSS dimensions. |
| Sharpness or raster output at different densities | Device pixels | Keep native captures and compare resolution and rendering as separate properties. |
| Responsive behavior across devices | Each device’s own viewport and DPR | Compare each device condition with its own baseline; do not resize distinct viewport layouts into apparent equivalence. |
MDN notes that page zoom affects devicePixelRatio, while pinch zoom does not. Moving a window between displays with different pixel densities can also change it. Record the value at capture time rather than assuming a machine’s display setting stayed constant. [MDN: devicePixelRatio]
2. Use a repeatable capture workflow
- Choose the comparison goal. For a regression test, hold the CSS viewport and environment constant. For a cross-device review, keep each device’s viewport and DPR as explicit conditions.
- Record capture metadata. Save CSS viewport width and height,
window.devicePixelRatio, browser and version, OS, zoom, and screenshot options alongside each image. - Stabilize page state. Use the same route, content, account state, locale, color scheme, and scroll position. Wait for fonts and images, and avoid capturing while animations or asynchronous updates are changing the page.
- Capture at CSS scale. For same-viewport layout comparisons in Playwright, pass
scale: 'css'to the screenshot call or configure the screenshot assertion’s scale. - Compare normalized images. Use a pixel diff or visual regression assertion. Set color and differing-pixel tolerances deliberately and inspect the resulting diff image.
- Keep original captures. Resampling is useful for alignment but can soften edges and cannot establish that the original browser render was identical.
3. Runnable Playwright example
This JavaScript example creates a Playwright Test visual assertion at a fixed CSS viewport, records DPR metadata, and uses CSS-pixel screenshot output. It also disables common animation noise. Install the test runner and browser with npm install -D @playwright/test followed by npx playwright install chromium, then save this as tests/home.spec.js and run npx playwright test.
const { test, expect } = require('@playwright/test');
test('home page matches the CSS-pixel baseline', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.evaluate(async () => {
if (document.fonts) await document.fonts.ready;
const images = Array.from(document.images);
await Promise.all(images.map(image => {
if (image.complete) return Promise.resolve();
return new Promise(resolve => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', resolve, { once: true });
});
}));
});
const metadata = await page.evaluate(() => ({
viewport: { width: innerWidth, height: innerHeight },
dpr: devicePixelRatio,
userAgent: navigator.userAgent
}));
console.log(JSON.stringify(metadata));
await expect(page).toHaveScreenshot('home.png', {
fullPage: true,
scale: 'css',
animations: 'disabled',
maxDiffPixelRatio: 0.001
});
});
Replace https://example.com with the page under test. The URL and test name determine your project-specific fixture and snapshot baseline. Playwright Test generates or updates baselines as part of its normal workflow; review baseline changes before accepting them. For reproducible regression checks, run the test in the same browser and platform as the baseline.
Important Playwright options
scale: 'css' | 'device': choose CSS pixels for layout comparisons or device pixels when raster density is what you are evaluating.fullPage: capture the full document or just the current viewport. Keep this consistent; content height changes can change full-page image dimensions.animations: 'disabled': disables finite animations and fast-forwards finite transitions for the assertion capture. It cannot make genuinely changing content deterministic.maxDiffPixelsandmaxDiffPixelRatio: cap the number or fraction of pixels allowed to differ. Choose one appropriate to the image size and review the diff.threshold: sets the tolerated perceived color difference for a pixel. A higher threshold is more permissive; do not use it to paper over unexplained changes.stylePath: apply a stylesheet to hide or stabilize volatile elements such as timestamps or rotating content. Playwright documents this option for improving screenshot determinism. [Playwright: Visual comparisons]
Playwright’s screenshot assertions use pixel comparison options, including threshold, maxDiffPixels, and maxDiffPixelRatio. A tolerance is a test policy, not a correction for a wrong scale or viewport. [Playwright: Visual comparisons]
4. Normalize screenshots you already have
If no browser capture can be repeated, first find the CSS viewport and DPR associated with each original. For a known 2× capture of a 1280 by 800 CSS viewport, a 2560 by 1600 raster can be resampled to 1280 by 800. Use a high-quality image resampling filter and retain the originals. Do not infer DPR solely from image dimensions: full-page captures, browser scaling, cropping, and differing CSS viewport sizes can produce misleading ratios.
Once normalized, confirm that the images depict the same CSS viewport and same page state. Resampling aligns pixel dimensions; it does not undo different font rasterization, browser behavior, image loading, or responsive breakpoints. If the viewport metadata is unknown, a dimension match alone is not enough to make a defensible layout comparison.
5. Separate DPR effects from real changes
Use this checklist when a diff appears:
- Raster dimensions differ by a consistent scale factor: check DPR and screenshot scale first.
- Element positions or wrapping differ after CSS-pixel normalization: verify CSS viewport dimensions, zoom, responsive breakpoints, and page state.
- Only text edges differ: check browser version, OS, fonts, and rendering environment. Font rasterization can vary between platforms.
- Images, timestamps, ads, or live data differ: wait for stable resources or mask/style the dynamic region.
- Only full-page captures differ in height: check content that loads on scroll, lazy images, and page state before capture.
Browser and OS versions, settings, hardware, power source, and headless mode can affect rendering. For regression baselines, keep the environment stable. For cross-browser or cross-platform coverage, maintain a baseline per environment or treat environment as an intentional test dimension. [Playwright: Visual comparisons]
6. Reliability, performance, and cost considerations
CSS-scale captures are smaller than high-DPR captures, which reduces storage and image-diff work when the goal is layout comparison. Keep high-resolution originals when sharpness or device-pixel rendering matters. For CI reliability, pin browser versions and run in a consistent environment; avoid concurrent tests that mutate shared data, and give slow pages enough time to reach a defined stable state.
Pixel comparison is sensitive to rendering conditions. Tolerances can reduce noise, but broad thresholds may conceal small visual regressions. Prefer targeted masks or styles for known volatile areas and keep the diff output available for review. The dossier provides no universal resampling algorithm or tolerance value: select these based on the visual risk of the page and the stability of your environment.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Images have different widths and heights, but layout looks the same | One was captured at device scale and the other at CSS scale, or DPR differs. | Record viewport and DPR; recapture both with scale: 'css' or normalize a known high-DPR image. |
| Diff is enormous after resizing | The CSS viewport or crop differs, or a 2× assumption was incorrect. | Compare capture metadata and page dimensions; do not resize based on dimensions alone. |
| Text differs despite matching dimensions | Different fonts, browser builds, OS, zoom, or rendering settings. | Match the baseline environment and wait for document.fonts.ready. |
| Diff changes between runs | Animations, live content, delayed images, timestamps, or unstable data. | Wait for required content, disable animations, and mask or style the specific volatile element. |
| Test reports a dimension mismatch | Viewport, full-page height, or screenshot scale differs. | Set the viewport explicitly and keep full-page and scale options consistent. |
| Too many pixels are tolerated | maxDiffPixels or maxDiffPixelRatio is too permissive. |
Reduce tolerance, inspect the diff, and separate known rendering noise from meaningful regions. |
| One device view is incorrectly treated as equivalent to another | Distinct CSS viewport layouts were forced to the same raster size. | Keep each viewport as its own test condition and compare to its own baseline. |
8. FAQ
Should I compare screenshots at 1× or 2×?
For same-viewport layout regression, compare at one pixel per CSS pixel. Use device scale when the high-density raster itself is part of what you are evaluating.
Does changing DPR change the CSS layout?
DPR describes the relationship between physical and CSS pixels; it does not by itself establish that the CSS viewport changed. Record viewport dimensions separately, because responsive layout depends on CSS viewport conditions.
Can image resizing make two different browsers equivalent?
No. It can align raster dimensions, but browser and platform rendering differences, including fonts, can remain.
Is an image diff proof that the page changed?
No. It identifies pixel differences. Check scale, viewport, environment, and page state before deciding whether a difference is a regression.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A single request captures a URL as PNG, JPEG, WebP, or PDF. Its API supports viewport and device presets, retina scale, full-page capture, and image resizing; see the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.


