Fix Blank Screenshots in Playwright When Using Headless Chromium
Trace blank headless Chromium screenshots from navigation through rendering and capture. Use runnable Playwright diagnostics to isolate the failing layer.
A blank screenshot in headless Chromium does not identify its own cause. First confirm that navigation succeeded and the expected content rendered; then inspect page errors and failed requests, wait for meaningful content, and compare screenshot modes and browser environments. A visible locator or a quiet network alone does not guarantee that the whole page has painted correctly.
The Playwright issue reports linked below describe different individual cases, not a single confirmed root cause. Treat each diagnostic step as a way to narrow the problem down.
1. Check navigation and the final page
Record the navigation response, final URL, and page title before capturing. If navigation times out or lands on an error page, investigate that first. One issue report describes a blank capture after navigation to an unreachable site; it is an example, not a universal rule. See the report about a blank screenshot after an unreachable-site navigation error.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
console.log({
status: response?.status() ?? null,
finalUrl: page.url(),
title: await page.title(),
});
await page.screenshot({ path: 'debug.png' });
} finally {
await browser.close();
}
A null response can occur for navigations that do not produce a standard HTTP response, such as some same-document transitions. Check the final URL and page content as well. An HTTP error status can still produce a document; inspect what actually rendered rather than assuming the status alone explains the image.
2. Wait for the content that matters
After navigation, assert on the heading, result, image, or other content the capture is supposed to contain. Use a web-first assertion that retries until it passes or times out. A report describes a blank toHaveScreenshot() result even though a locator was visible, which means visibility is useful evidence but not a guarantee of a correct full-page capture. See Playwright issue 21657.
import { test, expect } from '@playwright/test';
test('page renders expected content before capture', async ({ page }) => {
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
console.log('HTTP status:', response?.status() ?? 'no response object');
console.log('Final URL:', page.url());
await expect(page.getByRole('heading', { name: 'Example Domain' }))
.toBeVisible();
await page.screenshot({ path: 'page.png' });
});
Replace the example heading with an assertion specific to the target application. If the page uses client-side data, wait for a meaningful result state, not just the initial app shell. Check whether content is hidden, covered by an overlay, or absent because an API request failed.
3. Do not use network idle as proof of readiness
Playwright defines networkidle as having no network connections for at least 500 ms and discourages it as a test readiness condition. A page can be network quiet while still showing a loading shell or waiting on work that does not use the network. Prefer a content assertion; use navigation waits to control navigation, not to certify application readiness. See the Playwright API parameter documentation.
Navigation wait choices include commit, domcontentloaded, load, and networkidle. Choose the earliest event appropriate for the page, then separately wait for the state you need to capture. load can be unsuitable for pages that keep loading resources, while domcontentloaded does not imply that images or client-rendered data are ready.
4. Capture browser diagnostics
Log console errors, uncaught page errors, failed requests, and HTTP responses. This helps separate application failures from screenshot encoding or file-saving problems.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
page.on('console', message => {
if (message.type() === 'error') console.error('console:', message.text());
});
page.on('pageerror', error => console.error('page error:', error));
page.on('requestfailed', request => {
console.error('request failed:', request.url(), request.failure()?.errorText);
});
page.on('response', response => {
if (response.status() >= 400) {
console.error('HTTP error:', response.status(), response.url());
}
});
try {
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
console.log('navigation:', response?.status(), page.url());
await page.getByRole('heading', { name: 'Example Domain' }).waitFor({ state: 'visible' });
console.log('body text:', (await page.locator('body').innerText()).slice(0, 500));
await page.screenshot({ path: 'diagnostic.png' });
} finally {
await browser.close();
}
For a blank result, compare the document state, body text, expected locator, final URL, and diagnostic logs. If the page is visibly empty before the screenshot call, the problem is upstream of image capture.
5. Compare viewport, full-page, and element captures
Playwright supports viewport screenshots, full-page screenshots, locator screenshots, and screenshots returned as buffers. Capture more than one mode to see whether the symptom follows the page or a particular capture mode. This is a diagnostic comparison, not a guaranteed fix. See the Playwright screenshot guide.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.getByRole('heading', { name: 'Example Domain' }).waitFor();
await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'full.png', fullPage: true });
await page.getByRole('heading', { name: 'Example Domain' })
.screenshot({ path: 'heading.png' });
const png = await page.screenshot();
console.log('PNG bytes:', png.length);
} finally {
await browser.close();
}
If viewport and element captures contain content but full-page is blank or malformed, reduce the page height or test a smaller clip. Very tall documents and lazy-loaded sections can behave differently from the initial viewport; make sure relevant content is loaded before comparing outputs.
6. Match the browser and runtime environment
Use Playwright’s bundled Chromium unless you have a specific reason to use another executable. When comparing local, headed, headless, or CI results, hold these variables as steady as possible:
- Playwright release and its bundled Chromium build.
- Operating system and container image.
- Browser executable and launch arguments.
- Viewport size and device scale factor.
- Navigation outcome, final URL, and application data state.
- Readiness assertion and screenshot mode.
- Font and image loading state.
Playwright warns that rendering can vary with host OS, browser version, settings, hardware, power source, and headless mode. Keep visual baselines and comparisons in a matching environment. See Playwright’s guidance on visual comparisons.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.getByRole('heading', { name: 'Example Domain' }).waitFor();
await page.screenshot({ path: 'matched-environment.png' });
} finally {
await browser.close();
}
For a headed comparison, change only headless and keep the browser build, viewport, page state, and capture call the same. If headed and headless runs differ, the comparison narrows the investigation but does not by itself identify the cause.
7. Investigate fonts and other late resources when relevant
Screenshot preparation can wait for fonts, and font requests can fail or time out. Reports also describe font-related visual differences in full-page screenshot comparisons. Investigate fonts when logs mention font readiness, font requests fail, or typography and layout change; the reports do not establish fonts as a cause of every blank screenshot. See the font-wait screenshot report.
Check failed requests for font and image URLs, verify those resources are accessible from the same container, and inspect whether the expected page content appears before capture. Avoid adding arbitrary delays as the only readiness condition: they can make runs slower while still missing a variable load time.
8. Reduce the issue to a minimal reproduction
- Keep the exact URL or make a small local page that reproduces the rendering.
- Keep only navigation, one content-specific readiness assertion, and the screenshot call.
- Record Playwright version, bundled browser, OS or container, viewport, and launch options.
- Remove custom fixtures, screenshot helpers, and application setup one at a time.
- Compare viewport, element, and full-page captures.
- State whether the page is blank before capture, whether navigation returned a response, and whether the issue reproduces in a matching environment.
The available reports cover materially different situations, so a specific fix depends on a reproducible case.
Common errors and fixes
| Symptom | Likely cause to investigate | Next step |
|---|---|---|
| Navigation times out or final URL is an error page | Unreachable host, redirect issue, or failed navigation | Inspect the response, final URL, failed requests, and page content before debugging screenshot output. |
| Assertion passes but image looks blank | The asserted element is present while the rest of the page is not ready, is covered, or the capture mode differs | Inspect body text and diagnostics; compare viewport, locator, and full-page captures. |
| Works headed, fails headless | Different browser build, environment, launch settings, or rendering conditions | Use bundled Chromium and hold OS/container, viewport, browser version, and page state constant. |
| Visual test is flaky around fonts | Font requests fail or font readiness is delayed | Inspect failed font requests and screenshot logs; confirm fonts are reachable in the test environment. |
| Capture is blank only for a very tall page | Full-page capture or late/lazy content is implicated | Compare viewport and element captures, then reduce the page or capture a smaller region. |
| PNG file is zero bytes or missing | Capture did not complete or output handling failed | Await the screenshot promise, log its buffer length, and verify the output path and process permissions. |
Or skip the browser setup
If you need a screenshot without managing a browser runtime, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns a PNG, JPEG, WebP, or PDF. This can help when the task is obtaining a page capture; it does not diagnose why your own Playwright environment renders a blank page. 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,
)
r.raise_for_status()
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 Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie and consent banners as 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. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan. Learn more at ScreenshotNeo.
Sign up for 1,000 free screenshots a month, with no card required.
Performance, reliability, and cost notes
- Performance: Prefer a content-specific wait over a long fixed sleep. Capture only the needed region when full-page output is not required. Compare modes before attributing a delay to Chromium.
- Reliability: Keep the browser and operating system consistent for visual comparisons, and log navigation plus page diagnostics so failures are reproducible. A successful locator assertion is not proof that every part of the page rendered.
- Cost: In CI, avoid retrying blindly on blank output. Save diagnostic logs and a minimal reproduction first; repeated runs consume CI time without explaining the failure. If you use ScreenshotNeo instead for routine captures, its free tier is 1,000 monthly shots and paid tiers start at $5 for 3,000.
FAQ
Is headless Chromium inherently unable to render this page?
No. A blank image alone cannot establish that. Confirm navigation, page state, and environment, then compare capture modes.
Will adding a delay fix a blank screenshot?
Only if the page simply needs more time in that run, and fixed delays are fragile. Prefer waiting for the content the capture requires.
Should I switch to a different browser executable?
First reproduce with Playwright’s bundled Chromium. Introducing another executable adds a variable; use it only when there is a deliberate compatibility reason.
Does a visible locator guarantee that the screenshot will be correct?
No. It confirms that locator’s visibility condition. Check the rest of the page and compare the desired screenshot mode.


