PhantomJS Screenshots Differ From Chrome: Common Causes
PhantomJS and Chrome use different rendering engines. Compare viewport, capture bounds and page readiness before tracing visual differences to the renderer.
PhantomJS screenshots can differ from Chrome because the browsers use different rendering engines: PhantomJS uses an older WebKit engine, while Headless Chrome uses Blink. First make the comparison fair: match the layout viewport, capture region and mode, and page readiness. If those match and the pixels still differ, engine behavior may be the cause. For a Chrome visual test, generate both baseline and comparison screenshots with the same Chrome or Chromium build and capture path.
A timeout or viewport adjustment cannot make WebKit and Blink equivalent. The goal is to identify whether your mismatch comes from capture setup, page state or the renderer.
1. Start with a fair comparison
- Record the environment: browser versions, operating system or container image, URL, capture command or script, and output dimensions.
- Set the same layout viewport: match width and height before loading the page, then check the effective values when possible.
- Match capture bounds and mode: compare viewport with viewport, or full page with full page. A viewport and a crop are separate settings; full-page capture is a separate mode.
- Wait for equivalent page state: use a page-specific readiness signal for data, fonts, images or application rendering. Stabilize animation and time-sensitive content where appropriate.
- Compare the images: check their pixel dimensions before using a visual diff. If geometry and readiness match, investigate engine and environment differences.
PhantomJS documents viewportSize as the viewport used for layout, and separately documents clipRect for the capture region. Chrome’s headless documentation shows explicit window sizing and treats full-page screenshots as requiring additional steps. PhantomJS viewportSize, PhantomJS screen capture, Chrome headless documentation.
2. Compare viewport and capture bounds
The viewport controls layout. A width difference can change media-query selection, line wrapping and element positions. Height can also affect what is visible and page behavior. The capture rectangle determines which portion of the rendered page appears in the output. Matching one does not automatically match the other.
With PhantomJS, configure viewportSize before opening the page. If using clipRect, record its position and dimensions too. With Chrome, set the window size explicitly and ensure the screenshot command captures the same viewport or full-page region.
Check the actual image width and height from both tools before pixel-diffing. If the outputs have different dimensions, fix capture setup first; a diff will otherwise mix geometry differences with rendering differences.
3. Wait for the same visual state
A navigation or page-open callback does not prove that every application-specific visual change has finished. The PhantomJS examples include a short delay in one viewport example. Puppeteer’s Chrome example uses a network-idle condition. Those are examples of caller-selected waits, not universal guarantees that a page is visually stable.
Prefer a readiness condition tied to the page under test: for example, a selector that appears after data is rendered, or an explicit signal from the application. Account for web fonts, lazy images, asynchronous content and animations if they affect the screenshot. If content is intentionally dynamic, make the test data and capture moment deterministic where possible.
4. Understand engine differences
PhantomJS and Headless Chrome do not render through the same engine. Chrome for Developers describes PhantomJS as using an older WebKit version and Headless Chrome as using Blink. Differences can remain in layout, typography, CSS behavior, SVG or other rendered content even after geometry and page state are aligned. Chrome for Developers: Headless Chrome shell.
Choose the renderer based on what the screenshot is meant to represent:
- Current Chrome behavior: generate both baseline and comparison images with the same Chrome or Chromium build and automation path.
- Historical PhantomJS behavior: keep a PhantomJS baseline if that legacy output is the target. Do not expect Chrome to reproduce it exactly.
- Cross-browser behavior: treat each browser engine as its own expected output and compare like with like.
The newer Headless Chrome implementation runs the real Chrome browser; Chrome’s documentation distinguishes it from the older standalone headless shell. Record and pin the browser version and capture environment so an upgrade does not silently change the baseline.
5. Runnable capture examples
These examples use the same URL, viewport and readiness idea so you can build a controlled comparison. Replace the URL and selector with values from your page. PhantomJS is shown as a legacy comparison example; use your existing PhantomJS installation and script. For Chrome, Puppeteer provides a runnable Node.js path.
PhantomJS
var page = require('webpage').create();
page.viewportSize = { width: 1280, height: 800 };
page.open('https://example.com', function (status) {
if (status !== 'success') {
console.error('Could not load page');
phantom.exit(1);
return;
}
// Replace this delay with a page-specific readiness check when possible.
window.setTimeout(function () {
page.render('phantom.png');
phantom.exit();
}, 1000);
});
Set viewportSize before page.open. If your script uses clipRect, set the same crop in both tools or remove it for a viewport-only comparison.
Chrome with Puppeteer
Install Puppeteer in a Node.js project with npm install puppeteer, then save and run this script with Node.js. The example waits for a page-specific selector, which is usually a better signal than an arbitrary delay. Change [data-ready] to a selector your application exposes after rendering.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1280, height: 800 },
deviceScaleFactor: 1
});
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.waitForSelector('[data-ready]');
await page.screenshot({ path: 'chrome.png' });
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exit(1);
});
networkidle2 is a navigation condition, not a universal visual-readiness guarantee. Keep the selector wait if the page has one; remove or replace it if not. For a full-page comparison, use Puppeteer’s fullPage: true and choose the equivalent PhantomJS capture bounds and mode.
6. Troubleshooting common mismatches
| Symptom | Likely cause | Fix |
|---|---|---|
| Text wraps differently or columns shift | Different effective viewport width or responsive breakpoint | Set the viewport before navigation and inspect the effective width in both pages. |
| One image is taller or includes more content | Different capture mode, crop or full-page bounds | Compare image dimensions; align viewport, crop and full-page settings. |
| Fonts or images are missing in one output | Capture occurred before assets loaded, or the runtime environment differs | Wait for page-specific readiness and check the relevant asset requests and environment. |
| Content appears in one image but not the other | Different asynchronous page state, data, animation or time-sensitive content | Use the same test data and readiness signal; stabilize dynamic behavior where the test permits it. |
| Small visual changes remain after setup matches | WebKit-versus-Blink rendering differences, browser version or environment | Use the same engine and pinned browser build for baseline and test; investigate environment differences separately. |
| Chrome screenshot command fails or output is unexpected | Headless mode, window size or full-page procedure differs from assumptions | Follow the current Chrome headless documentation and specify window dimensions and capture mode explicitly. |
Font installation, device scale, CSS feature support and asset delivery are useful things to investigate, but the available documentation does not establish them as the cause of any particular mismatch. Confirm them against your own paired captures.
7. Performance, reliability and cost
For repeatable visual testing, reuse a consistent capture path and pin the browser version and operating environment. Avoid using a longer fixed sleep as the only readiness strategy: it can waste time on fast pages and still miss delayed rendering. A page-specific condition makes the capture point more meaningful, though it must reflect what the page actually needs.
Keep viewport, capture mode, readiness condition and browser build in the test configuration alongside the baseline. When upgrading Chrome, regenerate or review baselines deliberately because renderer changes can alter output. The supplied sources provide no benchmark or cost comparison between PhantomJS and Chrome, so choose based on the required rendering target and the maintenance needs of your test environment.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Make a GET request with the target URL to receive an image or PDF, without installing or managing a browser for the capture. 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://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo removes cookie banners, newsletter popups and chat widgets before the shot. Bot checks, blank pages and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card.
FAQ
Can Chrome be configured to match PhantomJS exactly?
No. Matching viewport, capture bounds and readiness improves the comparison, but different rendering engines can still produce different pixels.
Should I replace a PhantomJS visual baseline with Chrome?
Use Chrome if the intended target is Chrome. Generate and review a new baseline with the same Chrome build used for future comparisons.
Does waiting for network idle guarantee the page is ready?
No. It is one navigation condition. Use a readiness signal that reflects the content your page needs to show.


