How to Fix a Blank Website Screenshot in Chrome Headless
A blank screenshot can come from failed navigation, late content, the wrong target, or the runtime. Diagnose each layer with Puppeteer and Chrome CLI.
A blank PNG does not by itself mean Chrome’s screenshot function failed. First check whether navigation reached the intended page and whether the page contains visible content. Then check capture timing, the page or element being captured, viewport and headless mode, and finally the browser runtime. This order helps distinguish a page that was already blank from a screenshot that missed content that had not rendered yet.
The examples below use Puppeteer and Chrome’s command-line interface. Treat them as diagnostic patterns: replace the example URL and selectors with the site and visible content you expect. No single flag fixes every blank screenshot.
1. Confirm navigation reached the page you expect
Log the final URL, response status, title, body text, and browser console errors immediately before capture. A redirect, an HTTP warning page, a blocked request, or a failed navigation can leave you capturing a page that is technically present but does not contain the application.
import puppeteer from 'puppeteer';
const url = 'https://example.com/';
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
page.on('console', message => console.log('console:', message.type(), message.text()));
page.on('pageerror', error => console.error('page error:', error.message));
page.on('requestfailed', request => {
console.error('request failed:', request.url(), request.failure()?.errorText);
});
const response = await page.goto(url, {waitUntil: 'networkidle2', timeout: 60000});
console.log({
requestedUrl: url,
finalUrl: page.url(),
status: response?.status(),
title: await page.title(),
bodyText: (await page.locator('body').innerText().catch(() => ''))?.slice(0, 1000),
});
await page.screenshot({path: 'page.png'});
} finally {
await browser.close();
}
networkidle2 is a useful initial wait condition, as shown in the Puppeteer screenshot guide. It does not prove that a single-page application has finished rendering its useful content. Inspect the logged final URL and status, and look for console errors, failed requests, access-denied pages, and redirects. If navigation throws, fix that error before investigating the image file.
For HTTP URLs, Puppeteer documents cases where Chrome for Testing can show an HTTPS-first warning page, and remote HTTP hosts may encounter net::ERR_BLOCKED_BY_CLIENT. Check the final page and browser logs rather than assuming the screenshot itself is at fault. See Puppeteer troubleshooting.
2. Check whether JavaScript produced the expected DOM
Chrome’s --dump-dom option prints the DOM after Chrome parses the HTML and runs page scripts. That differs from fetching the original HTML source: a JavaScript-rendered application may produce a different DOM after startup.
chrome --headless --dump-dom https://example.com/
Use the Chrome binary available in your environment if its executable is not named chrome. Search the output for the application’s expected heading or root content. If it is missing, investigate navigation, application startup, JavaScript exceptions, and failed network requests. If the DOM has the content but it is not visible, inspect styles, responsive breakpoints, overlays, and whether the content is hidden.
3. Wait for visible application content before capture
Wait for a meaningful selector after navigation. Replace #app main with a selector that appears when the page is genuinely ready. A fixed delay can be useful as a diagnostic experiment, but it is less dependable as a permanent readiness check because load time varies.
import puppeteer from 'puppeteer';
const url = 'https://example.com/';
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
await page.setViewport({width: 1280, height: 900});
const response = await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 60000,
});
if (!response) throw new Error('Navigation did not return a main resource response');
if (!response.ok()) throw new Error(`Unexpected HTTP status: ${response.status()}`);
await page.waitForSelector('#app main', {visible: true, timeout: 20000});
console.log({url: page.url(), status: response.status(), title: await page.title()});
await page.screenshot({path: 'page.png', fullPage: true});
} finally {
await browser.close();
}
For a page that does not have a stable selector, wait for a specific piece of text or another app-specific condition with page.waitForFunction(). Avoid treating network idleness alone as proof of readiness: some pages keep connections open, while others become idle before client-side content appears. Puppeteer’s screenshot guide documents Page.screenshot() and element screenshots.
Chrome CLI offers separate timing controls. --timeout=5000 delays capture up to five seconds; --virtual-time-budget advances time-dependent page work in virtual time. These can help reproduce timing issues, but use a page-specific readiness condition in Puppeteer when you can.
chrome --headless --timeout=5000 --screenshot=page.png --window-size=1280,900 https://example.com/
4. Verify the page, element, and viewport being captured
Make sure the same Puppeteer Page instance that navigated is the one used to take the screenshot. If the desired output is one element, wait for that element to become visible and capture its handle rather than assuming a page-level screenshot will frame it correctly.
const card = await page.waitForSelector('.report-card', {visible: true, timeout: 20000});
if (!card) throw new Error('Report card was not found');
await card.screenshot({path: 'report-card.png'});
Puppeteer’s ElementHandle.screenshot() attempts to scroll an off-screen element into view. If the element exists but its screenshot is blank, inspect its dimensions and computed visibility:
const details = await page.$eval('.report-card', element => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
width: rect.width,
height: rect.height,
display: style.display,
visibility: style.visibility,
opacity: style.opacity,
};
});
console.log(details);
For full-page output, use fullPage: true. For a viewport screenshot, set explicit dimensions before navigation or capture. Check that the chosen width does not trigger a mobile layout with different content, and that the target is not outside a clipped region. Puppeteer’s screenshot options document settings such as fullPage, clip, image type, and background transparency.
5. Compare Chrome headless modes and versions
Do not assume that old and new headless modes behave identically. Chrome’s newer headless implementation shares Chrome code; the older implementation was separate. Puppeteer documents the old mode as chrome-headless-shell, which does not match regular Chrome completely. Check the Puppeteer headless modes guide and record the installed Puppeteer and Chrome versions before comparing results.
import puppeteer from 'puppeteer';
console.log('Puppeteer browser version:', await (async () => {
const browser = await puppeteer.launch({headless: true});
try { return await browser.version(); }
finally { await browser.close(); }
})();
Run the same URL, viewport, wait condition, and capture target with the modes supported by your installed Puppeteer release. Compare the DOM and visible page state as well as the resulting image. Avoid copying old mode mappings from an old article without checking the version-specific documentation for your installation.
6. Investigate rendering and GPU only when evidence points there
If ordinary text and layout render but a WebGL or canvas area is blank, identify which Chrome binary Puppeteer launched. Puppeteer’s troubleshooting guide says chrome-headless-shell needs --enable-gpu to enable GPU acceleration in headless mode:
const browser = await puppeteer.launch({
headless: 'shell',
args: ['--enable-gpu'],
});
This guidance is specific to the shell mode and GPU acceleration. Do not add GPU flags as a universal fix for blank pages. Compare the same capture using regular Chrome and inspect browser logs when the missing content is GPU-dependent.
7. Check Linux and CI runtime dependencies
A page that works locally but fails in a container may have a different browser binary, missing shared libraries, unavailable fonts, or blocked browser installation. On Linux, Puppeteer recommends checking missing Chrome dependencies with:
ldd /path/to/chrome | grep 'not found'
Install the missing dependencies for your distribution and ensure the browser binary exists. If an install policy blocked Puppeteer’s browser download, follow its documented browser installation command:
npx puppeteer browsers install
Fonts can cause missing glyphs or fallback typography; lack of a font alone is not proof of a completely blank page. Check for additional font requirements when only text rendering differs. Puppeteer’s troubleshooting guide lists Linux dependencies and browser setup details. Keep the browser, Puppeteer package, and runtime configuration consistent between local and CI runs where practical.
8. Inspect the invisible page in DevTools
Remote debugging lets you inspect a headless page’s live state. Start Chrome with a debugging port and the target URL:
chrome --headless --remote-debugging-port=0 https://example.com/
Chrome prints a WebSocket endpoint to standard output. In a headful Chrome instance, open chrome://inspect, configure the host and port from that endpoint, then inspect the remote target. The Chrome headless debugging guide describes this workflow. Inspect the page around the time the screenshot would be taken to establish whether it was already blank or whether the capture path missed visible content.
Quick diagnosis table
| Evidence | Likely area | Next check |
|---|---|---|
| Unexpected final URL or error status | Navigation, redirect, access page | Log URL, response, console, failed requests |
Expected app content absent from --dump-dom |
App startup or page scripts | Inspect JavaScript and network errors |
| DOM has content, screenshot is early | Capture timing | Wait for a visible app selector |
| Only a component is blank or clipped | Target, visibility, dimensions | Inspect its rect, styles, and selector |
| Only one viewport size fails | Responsive layout or viewport | Set explicit dimensions and compare breakpoints |
| Canvas/WebGL only is blank in shell mode | GPU/rendering path | Identify binary; test shell-specific GPU guidance |
| Local works, CI fails | Browser install or OS dependencies | Check binary, missing libraries, fonts, and versions |
Troubleshooting common errors
| Symptom or error | Cause to check | Fix or diagnostic |
|---|---|---|
Navigation timeout |
Slow page, persistent requests, or unreachable host | Inspect failed requests and choose a meaningful app selector; adjust timeout only if the page legitimately needs longer. |
net::ERR_BLOCKED_BY_CLIENT |
Request blocked by browser policy or environment | Inspect the request and final page; follow Puppeteer’s troubleshooting guidance for the specific HTTP-host case. |
| HTTPS warning page captured | Chrome for Testing HTTPS-first behavior on an HTTP URL | Check final URL and page content; resolve the URL or warning condition before screenshot capture. |
No usable sandbox! |
Host/container is not configured for Chrome’s sandbox | Configure the host sandbox according to Puppeteer’s troubleshooting guide; do not treat disabling sandboxing as a general blank-image fix. |
error while loading shared libraries or missing libraries |
Runtime dependencies absent from Linux image | Use ldd /path/to/chrome | grep 'not found' and install required packages. |
| App selector wait times out | Wrong selector, app did not start, or content is not visible | Check --dump-dom, console errors, and actual page structure; use a selector tied to ready content. |
| Screenshot has a tiny/empty element | Element is hidden, zero-sized, or not the intended match | Inspect bounding box, visibility, selector matches, and responsive layout. |
Performance, reliability, and cost considerations
Use a readiness condition that represents the content you need. Waiting for a fixed long delay on every page can waste time, while capturing immediately can produce incomplete output. Navigation timeout and selector timeout should be finite so a broken page does not hang a job indefinitely. Close the browser in a finally block, as in the examples, so failures do not leave Chrome processes behind.
Full-page captures can require more rendering and memory than viewport captures, especially on long pages. Capture only the needed element or viewport when that meets the requirement. Keep logs useful but avoid recording secrets from URLs, headers, cookies, or page text. In CI, stable browser versions and explicit viewport settings make failures easier to reproduce.
Or skip the browser setup
If you need a screenshot without maintaining Chrome and Puppeteer setup, ScreenshotNeo is a website screenshot API and MCP server. Its one-call API returns an image or PDF; the same parameter names used by other screenshot APIs also work. 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}`);
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 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. Every feature is on every plan.
Sign up free for 1,000 screenshots a month, no card required.
FAQ
Does a blank screenshot mean the PNG is corrupt?
No. First determine whether the page itself was blank at capture time by checking the DOM, logs, and live page state.
Should I always use networkidle2?
It is a useful navigation wait condition, but it does not guarantee that an application-specific view is ready. Wait for meaningful visible content too.
Should I always add --enable-gpu?
No. The documented requirement applies to GPU acceleration in chrome-headless-shell. Use it only when the binary and rendering symptoms make it relevant.
Is switching to headful Chrome the fix?
It is a useful comparison. If headful works and headless does not with the same URL, viewport, and wait, investigate mode differences and runtime state before settling on a workaround.


