Headless and headed Puppeteer screenshots differ when their rendering inputs differ. Headless Chrome draws into a configurable virtual screen, while headed Chrome uses the operating system’s physical displays. Viewport and device scale factor, GPU compositing, fonts and native libraries, page-readiness timing, and screenshot options can all change the final pixels—even when the URL and JavaScript are identical.
To make captures comparable, pin the browser build, explicitly set viewport and device scale factor, model the same screen geometry, use the same GPU path and dependencies, wait for the same ready state, and keep every screenshot option identical.
What actually changes between headless and headed mode?
“Headless” is not simply headed Chrome with the window hidden. Chrome headless uses a virtual screen whose origin, dimensions, scale factor, orientation and work area can be configured. Headed Chrome receives those values from the desktop, window manager and attached monitor. A laptop display at 150% scaling, a remote desktop session, and a Linux CI container can therefore produce different CSS-to-device-pixel mappings.
Puppeteer adds another layer: page.setViewport() defines a CSS viewport, while deviceScaleFactor controls how many device pixels represent each CSS pixel. If either value is implicit, responsive breakpoints, text rasterization and image dimensions may change before the screenshot is taken.
#1 Best Overall
The capture itself also has semantics. Full-page stitching, clipping, surface capture, transparent backgrounds and the selected image type can produce different bitmaps even when layout is identical.
The eight sources of pixel differences
1. CSS viewport and responsive breakpoints
Media queries react to CSS pixels, not the physical monitor’s diagonal size. A headed window that is 1280 CSS pixels wide may become a 1024-pixel viewport after browser chrome, dock areas or display scaling are applied. A headless run configured for 1280 by 800 can select a different navigation menu, grid, font size or image source.
Set width and height explicitly in every environment:
await page.setViewport({
width: 1280,
height: 800,
deviceScaleFactor: 1
});
Record the resulting viewport with await page.evaluate(() => ({ innerWidth, innerHeight, devicePixelRatio })) and compare those values before comparing pixels.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →2. Device scale factor and rasterization
deviceScaleFactor: 1 creates one device pixel per CSS pixel. A value of 2 creates a retina-style bitmap with twice the width and height in device pixels (four times as many pixels overall). Fractional or system-derived scale factors alter glyph antialiasing, borders and image resampling. Puppeteer’s documented default is 1; setting it to 0 restores the system default, which is unsuitable for deterministic visual tests.
Do not compare a 1280×800, DPR 1 image with a 2560×1600, DPR 2 image. Normalize dimensions and DPR first, or downsample both with the same tool after capture.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
3. Physical versus virtual screen geometry
Headed Chrome inherits the monitor’s work area, origin and orientation. Multiple monitors can even produce negative screen coordinates. Headless Chrome has no physical display; its virtual screen can be controlled with Chrome flags such as --window-size and, where supported, --screen-info. That screen-info model includes origin, size, scale factor, orientation and work area.
If a page calls screen APIs, positions a popup, or uses viewport segments, matching only page.setViewport() is not enough. Model the headed display’s geometry in headless mode and keep browser window arguments consistent.
Free tools Windows power users keep installed
One-click scans. No signup required.
4. GPU and compositing path
GPU availability changes compositing, antialiasing and the rendering of transforms, filters, canvas and video. Containers often lack a usable driver. Puppeteer’s chrome-headless-shell disables GPU compositing unless launched with --enable-gpu; the available graphics drivers still determine whether that path works.
Use the same Chrome product and arguments locally and in CI. If the headed reference uses GPU rendering, either provide an equivalent GPU environment or deliberately disable GPU in both runs. Treat a change in GPU flags as a rendering change, not a harmless performance tweak.
5. Fonts and native graphics libraries
When a requested web font is unavailable or still loading, Chrome substitutes another face. Different glyph widths alter line wrapping, element heights and every pixel below the changed line. Linux environments also need consistent native libraries. Puppeteer’s troubleshooting guidance specifically calls out packages such as fonts-liberation, libcairo2, libpango-1.0-0 and libgbm1.
Install the same font files and library versions in the developer image and CI image. Avoid relying on fonts installed interactively on a workstation. Verify readiness with the Font Loading API:
Rank #3
await page.evaluate(async () => {
await document.fonts.ready;
return [...document.fonts].map(f => ({ family: f.family, status: f.status }));
});
6. Page readiness and timeout policy
A screenshot taken while a web font, image, data request or animation is in flight is a different test from one taken after the page settles. Network speed and CPU load make this especially visible in CI. Chrome’s headless capture timeout bounds how long a capture waits; an inconsistent timeout can leave one run partially rendered.
Choose one navigation policy (for example, networkidle2 plus explicit application readiness), wait for fonts and critical images, and freeze animations where visual stability matters:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('#app[data-ready="true"]', { timeout: 30000 });
await page.evaluate(async () => {
await document.fonts.ready;
await Promise.all([...document.images].map(img => img.complete
? Promise.resolve()
: new Promise(resolve => { img.addEventListener('load', resolve, { once: true }); img.addEventListener('error', resolve, { once: true }); })));
});
await page.addStyleTag({ content: '* { animation: none !important; transition: none !important; caret-color: transparent !important; }' });
Use a bounded timeout and handle timeout failures explicitly; never turn an early, blank capture into a “successful” baseline.
7. Screenshot options and capture semantics
Keep these options identical:
fullPage: captures the document’s full height rather than only the viewport.clip: limits capture to a rectangle in CSS pixels.captureBeyondViewport: controls whether content outside the current viewport can be captured.fromSurface: captures the compositor surface; Puppeteer’s default istrue.omitBackground: makes the page transparent instead of compositing the default background.type: PNG, JPEG and WebP use different encoding and, for JPEG/WebP, potentially lossy pixels.
A full-page screenshot may trigger lazy loading as the page is scrolled or stitched. Compare like-for-like options and, for pixel tests, prefer PNG.
Recommended Free Tools
8. Browser and operating-system versions
Pin the same Puppeteer version and Chrome-for-Testing build in local and CI runs. Browser updates can change font shaping, CSS implementation, image codecs and GPU behavior. Capture the browser version, command-line arguments, viewport, DPR, installed fonts and screenshot options as test metadata so a future diff has an identifiable cause.
A deterministic Puppeteer setup
The following Node.js example uses explicit values and waits for application readiness. Adapt the selector to your page.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
// Keep these arguments identical in headed and headless jobs.
args: ['--window-size=1280,800']
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 60000
});
await page.waitForSelector('#app[data-ready="true"]', { timeout: 30000 });
await page.evaluate(async () => {
await document.fonts.ready;
await new Promise(requestAnimationFrame);
await new Promise(requestAnimationFrame);
});
await page.screenshot({
path: 'shot.png',
type: 'png',
fullPage: true,
captureBeyondViewport: true,
fromSurface: true,
omitBackground: false
});
await browser.close();
For a headed reference, change only headless: true to headless: false (and run in a display-capable environment). Keep the viewport, arguments, readiness code and screenshot options unchanged. If the headed display uses a different scale factor, either configure that display to match or treat the run as a separate baseline.
Reproducibility checklist
- Pin Puppeteer and Chrome-for-Testing versions.
- Set CSS width, height and
deviceScaleFactor; never depend on system defaults. - Match virtual and physical screen geometry with
--window-sizeor--screen-info. - Use identical headless/headful mode, GPU flags and Chrome arguments; add
--enable-gpufor headless-shell only when the test requires GPU compositing and the driver is available. - Install matching fonts and Linux graphics libraries.
- Use identical
fullPage, clipping, surface, background and image-type settings. - Wait for the same application-ready signal, fonts, images and animation state under one bounded timeout policy.
- Compare image dimensions and metadata before running a pixel diff.
How to diagnose a mismatch
Start with dimensions and environment
Log innerWidth, innerHeight, devicePixelRatio, screenshot byte size, browser version and launch arguments. A dimension mismatch almost always indicates viewport, DPR, full-page height or clipping differences rather than a CSS bug.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallClassify the visual symptom
- Different layout or line breaks: check CSS viewport, fonts, web-font readiness and browser version.
- Everything is the same shape but edges look softer: check DPR, GPU path and image type.
- Only below-the-fold content differs: check
fullPage, lazy loading and capture timing. - Blank, partially loaded or missing images: check navigation/readiness waits, network failures and timeout handling.
- Transparent versus solid background: check
omitBackgroundand the image decoder used by the diff tool.
Reduce to a controlled experiment
Capture the same URL with a fixed viewport, DPR 1, PNG output and a small clip. Then add full-page capture, GPU, custom fonts and other features one at a time. This isolates the first variable that changes pixels instead of masking several causes in one large diff.
Performance, reliability and cost considerations
Headless mode generally avoids window-management overhead, but deterministic waits can make a capture slower than an immediate screenshot. Waiting for network idle may never settle on pages with analytics or long polling; an application-specific ready selector plus a maximum timeout is more reliable. Full-page captures consume more memory as page height grows, and DPR 2 multiplies bitmap memory by roughly four compared with DPR 1.
For CI, reuse a browser process when safe, but create a fresh page and clear state between cases. Use a fixed timezone, locale, geolocation, cookies and authorization headers when those values affect rendered content. Store the environment metadata next to each baseline. If a capture fails, fail the test rather than accepting a blank or bot-check page as a new baseline.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server when you need a consistent capture without maintaining Chrome locally. One GET request returns PNG, JPEG, WebP or PDF. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsUse the API documentation at https://screenshotneo.com/docs/ for all options. A minimal cURL capture is:
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The same request in Python:
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)
And in Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also supports full-page and element captures, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, pre-capture clicks, selector hiding, selector/delay/network-idle waits, request blocking, custom headers/cookies/user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.
An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients, so an AI agent can request captures directly. Every feature is included on every plan: 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. Yearly billing provides two months free.
Create a free ScreenshotNeo account to try 1,000 screenshots a month without a card.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Frequently Asked Questions
Should visual tests use headless or headed Chrome?
Use whichever mode matches the environment you ship or review, then keep that mode fixed for baselines. Switching modes requires revalidating viewport, screen geometry, GPU and dependencies.
Why do screenshots have different dimensions even with the same viewport?
Device scale factor, full-page height, clipping, or a system-derived display scale can change device-pixel dimensions. Log CSS viewport and devicePixelRatio before diffing.
Can I fix every difference with a larger delay?
No. Delays address readiness races, not different fonts, DPR, GPU compositing, screen geometry or screenshot semantics. Control those inputs explicitly.
Is JPEG suitable for pixel-perfect comparisons?
Usually not. JPEG is lossy; use PNG for baselines unless your production requirement specifically tests JPEG output.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

