Chrome Headless Screenshots Differ from Chrome Browser: Common Causes
Headless screenshots can differ because of browser mode, viewport, page timing or graphics settings. Use this checklist to isolate the cause and make captures repeatable.
Chrome Headless screenshots usually differ from visible Chrome because the runs do not share the same browser implementation, viewport and screen scale, page state at capture time, or graphics environment. Start by recording the Chrome binary and version, then align those settings and capture both runs after the same page-ready condition. Chrome’s newer Headless mode shares Chrome’s browser code; its earlier Headless implementation was separate. There is no single cause that explains every mismatch.
1. Check the Headless implementation and Chrome build
Chrome introduced its newer, unified Headless mode in Chrome 112. The older Headless implementation was a separate browser implementation, so comparing it with visible Chrome can expose implementation differences. Automation libraries may select a mode according to their own settings and version. Record the actual executable, browser version, automation-library version, and mode for every comparison. See Chrome’s Headless mode documentation.
With Puppeteer, make the choice explicit. Depending on the Puppeteer version, headless: true may select a different mode than headless: 'shell'; check the documentation for the version installed in your project. To compare with visible Chrome, set headless: false while keeping the rest of the test configuration constant.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
// Set executablePath when you need a specific Chrome binary.
// executablePath: '/path/to/chrome',
});
console.log('Browser version:', await browser.version());
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.screenshot({ path: 'headless.png' });
await browser.close();
Use the same Chrome binary for both runs where possible. Pin the browser version in CI so an automatic browser update does not silently change rendering. If you use a separate headless-shell binary or an automation framework that downloads Chrome, record that fact too.
2. Match viewport dimensions and device scale
A screenshot’s pixel dimensions are affected by the CSS viewport and device scale factor. A changed viewport can activate a responsive breakpoint, reflow text, or change which assets load. A changed scale factor can alter rasterized dimensions and fine edges even when the CSS layout is identical. Confirm whether your reference is measured in CSS pixels or image pixels.
For a simple CLI capture, specify the window size. Chrome’s Headless documentation shows --window-size for screenshots. For automated comparisons, set the viewport and device scale factor explicitly in the automation framework, and align screen properties when the framework exposes them.
chrome --headless --window-size=1440,900 --screenshot=shot.png https://example.com
Chrome’s virtual-screen feature can control screen properties such as size and scale factor; see the virtual screens documentation. Do not assume a desktop window size, viewport, and screenshot dimensions are interchangeable—check the output image dimensions and the CSS viewport separately.
3. Capture the same page state
Two captures can differ when taken at different points in page loading. Dynamic text, delayed images, web fonts, animations, rotating banners, video, clocks, and network responses may still be changing. A fixed delay can make a capture more repeatable, but it does not prove that the relevant content is ready.
Prefer a page-specific readiness condition. Wait for a selector that identifies the content you need, then wait for fonts and images where they matter. Disable or freeze animations and time-dependent content in a controlled test environment if they cause noise.
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-test="report-ready"]');
await page.evaluate(async () => {
if (document.fonts?.ready) await document.fonts.ready;
const images = [...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 });
});
}));
});
await page.screenshot({ path: 'ready.png', fullPage: true });
Waiting for every image may not suit pages with intentionally lazy-loaded content or assets that never resolve. In those cases, scroll the relevant content into view and wait for the specific images or section required by the comparison.
The Chrome CLI supports --timeout and --virtual-time-budget as capture timing controls. A timeout waits for a fixed duration; virtual time advances time-dependent page work. These controls are not a substitute for checking that your particular page is ready.
chrome --headless --timeout=5000 --window-size=1440,900 --screenshot=shot.png https://example.com
chrome --headless --virtual-time-budget=5000 --window-size=1440,900 --screenshot=shot.png https://example.com
4. Account for fonts, operating system and graphics
Record the operating system or container image, installed fonts, and graphics drivers alongside the browser build. These are variables to investigate, especially when text metrics or antialiasing differ; they are not a proven universal explanation for every Headless-versus-visible mismatch. A missing font can change wrapping and page height, while different graphics conditions may affect some rendered content. Confirm a suspected cause with a controlled reproduction.
Headless Chrome can use the local GPU in some circumstances. Chromium’s GPU in Headless Chrome guide describes Linux GPU autodetection requirements, including X display and driver configuration, and notes Vulkan as one possible route in some setups. Do not treat --disable-gpu as a universal correction: compare the actual graphics configuration and change one variable at a time.
5. A repeatable diagnostic workflow
- Record the environment. Log the exact browser executable and version, automation-library version, mode, OS or container image, fonts, and relevant graphics configuration.
- Align geometry. Set equal CSS viewport width and height, device scale factor, and screen properties. Check the resulting image dimensions.
- Align readiness. Wait for the same meaningful page condition in both runs. Account for fonts, images, animations, and dynamic data.
- Repeat on one machine. Capture both modes on the same host to reduce environmental variables. Change one setting per comparison.
- Inspect the live Headless page. Chrome’s Headless announcement describes connecting DevTools to a Headless target using
--remote-debugging-port. Inspect the DOM, computed styles, loaded resources, and page state to distinguish layout or state differences from final pixel capture differences. - Localize the diff. Compare the images and identify whether the change affects text, overall geometry, or particular rendered regions. These patterns are diagnostic clues, not definitive cause mappings.
chrome --headless --remote-debugging-port=9222 https://example.com
For a useful visual comparison, preserve the original screenshots and record the capture settings next to each file. Compare same-sized images, and avoid resizing one image to fit the other before inspecting differences: resizing can hide a viewport or scale mismatch.
6. Common causes and fixes
| Symptom | Likely variable to check | Next step |
|---|---|---|
| Most of the page shifts or reflows | Viewport, scale factor, responsive breakpoint, or page state | Set the same viewport and scale; confirm the page is ready before capture. |
| Text wraps differently or glyph edges change | Font availability, browser build, OS, or rasterization environment | Compare installed fonts and reproduce on the same host and browser build. |
| Images or sections are missing | Capture happens before load, lazy loading, failed requests, or different page state | Wait for the relevant selector and assets; inspect network activity and scroll lazy content into view. |
| Only animated or changing content differs | Capture timing, animation, clock, or live data | Use a deterministic readiness condition and freeze or control changing inputs where possible. |
| Canvas, WebGL, or composited regions differ | Graphics backend, driver, host, or browser build | Record and compare graphics conditions on the same machine. Treat this as a hypothesis to verify. |
| Headless and visible runs behave very differently | Different Headless implementation or Chrome binary | Log the binary and mode; make the mode explicit and compare using the same build. |
7. Performance, reliability and cost
For repeatable visual tests, browser startup, page load, and readiness waits all contribute to capture time. A short fixed wait may reduce latency but produce intermittent screenshots when the page is slower; a long fixed wait wastes time without guaranteeing readiness. Use a meaningful selector or application-provided ready signal, and set a bounded timeout with useful failure logs.
Pinning the browser and container environment improves comparability, but it means updates should be intentional: upgrade the browser, review resulting image changes, and then update the baseline. Keep the screenshot, browser version, viewport, scale, and readiness condition together so a later mismatch is diagnosable. The Chrome documentation does not establish a universal performance or cost figure for these choices; measure them in your own capture environment.
Or skip the browser setup
For a one-request website capture, ScreenshotNeo returns an image or PDF without requiring you to configure a local Chrome process. Its API accepts screenshot options, and its parameter names also work with those used by other screenshot APIs. See the ScreenshotNeo API documentation.
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}`);
Cookie banners, popups, and chat widgets are removed before the shot, and each of those steps can be turned off. Bot checks, blank pages, and failed loads are never billed; response headers report the page verdict and billing status. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does Headless mean Chrome uses a different browser engine?
Chrome’s newer Headless mode shares regular Chrome code. The older Headless implementation was separate, so verify which mode and binary your tooling launches.
Should I always disable GPU to match visible Chrome?
No. GPU behavior depends on the host and graphics setup. Compare the actual environments and test a controlled change instead of assuming one flag fixes every mismatch.
Is a fixed sleep enough to make screenshots stable?
Not by itself. A fixed delay can help with predictable pages, but a page-specific readiness condition is more reliable when content, fonts, and images load at variable times.


