How to Fix White or Gray Screenshots in Puppeteer
A blank Puppeteer screenshot usually means the page was not ready or the capture settings are wrong. Follow this sequence to isolate navigation, rendering, viewport, scale, and clipping problems.

A white or gray Puppeteer screenshot usually points to one of two problems: the page had not rendered meaningful content when the capture ran, or the screenshot settings captured the wrong region or background. Start with a known viewport and a simple PNG, confirm that navigation succeeded, then wait for an element that proves your application is ready. Only after those checks should you investigate device scale, clipping, or full-page capture.
Puppeteer’s baseline is direct: “For capturing screenshots use Page.screenshot().” Its screenshots guide shows navigation, a wait condition, page.screenshot(), and browser cleanup. The sequence below adds practical diagnostics so you can identify which layer is failing. Puppeteer Screenshots guide.
1. Start with a small, deterministic capture
Set the viewport before navigation. Begin at deviceScaleFactor: 1, omit clip and fullPage, and explicitly request PNG. This removes several variables at once. Puppeteer documents viewport width and height in CSS pixels and a default device scale factor of 1. In some mobile or touch emulation cases, changing viewport settings can reload the page, which is another reason to configure them before goto(). See the Viewport reference and Page API.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({
width: 1365,
height: 900,
deviceScaleFactor: 1,
});
const response = await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 60000,
});
console.log('URL:', page.url());
console.log('HTTP status:', response?.status());
console.log('Title:', await page.title());
console.log('Body text:', (await page.locator('body').innerText()).slice(0, 500));
await page.screenshot({ path: 'debug.png', type: 'png' });
} finally {
await browser.close();
}
})();
Replace the example URL and probe a piece of content your application is expected to show. A successful goto() does not establish that a client-rendered app has finished rendering. Similarly, a non-empty body can contain only a shell, loading placeholder, or error state. Treat status, title, body text, and the screenshot as separate clues.
2. Wait for application content, not just navigation
waitUntil: 'networkidle2' is a useful baseline when requests settle, but it is not a universal “the app is ready” signal. A single-page application may fetch data after navigation, render after an asynchronous state update, or keep analytics and streaming connections active. Prefer a selector that represents finished content, or wait for an application-specific readiness flag. A network-idle wait can be an additional condition when appropriate; it should not replace the app’s own readiness signal.
const response = await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
timeout: 60000,
});
// Wait for a real, visible piece of application content.
await page.waitForSelector('[data-testid="dashboard-ready"]', {
visible: true,
timeout: 30000,
});
// Optional: wait for a quiet network window if the app settles its requests.
await page.waitForNetworkIdle({ idleTime: 800, timeout: 15000 }).catch(() => {
console.warn('Network did not become idle; the content selector did appear.');
});
await page.screenshot({ path: 'dashboard.png', type: 'png' });
If you control the app, expose a deliberate readiness condition after the data and UI needed for capture are available:
// Application code: set this only after the view is ready to capture.
window.__SCREENSHOT_READY__ = true;
// Capture code:
await page.waitForFunction(() => window.__SCREENSHOT_READY__ === true, {
timeout: 30000,
});
await page.screenshot({ path: 'ready.png', type: 'png' });
Use a fixed delay only as a temporary diagnostic or when the page has no observable readiness condition. A delay may appear to fix a race on a fast machine but fail under slower network, CPU contention, or a different page state. Puppeteer provides waitForNetworkIdle(), waitForFunction(), and selector waiting on the Page API.
3. Log browser errors and failed requests
Before changing capture flags, find out whether the app failed to render. Listen for console errors, uncaught page exceptions, failed requests, and HTTP responses. These event logs are a diagnostic procedure: they help distinguish a broken application or missing asset from a screenshot-region problem.
page.on('console', message => {
if (message.type() === 'error') console.error('PAGE CONSOLE:', 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);
});
page.on('response', response => {
if (response.status() >= 400) {
console.error('HTTP ERROR:', response.status(), response.url());
}
});
Register the listeners before goto(), then navigate and wait for the meaningful selector. Inspect failures for JavaScript bundles, stylesheets, fonts, images, and the API calls that populate the view. Check whether the capture environment has the required authentication, cookies, environment configuration, or access to the target host. If the page shell renders but data does not, the screenshot can be correctly capturing an application that is stuck in a loading state.
4. Remove options that can hide the rendered page
Once the page content exists, simplify the capture. An accidental clip rectangle can point outside the useful content or cover only a blank area. Remove it and capture the viewport first. Add fullPage: true only when you actually need the full document. The screenshot options reference defines clip as a region and fullPage as a full-document capture option.
// Minimal viewport capture
await page.screenshot({ path: 'viewport.png', type: 'png' });
// Full document, only after the basic capture works
await page.screenshot({ path: 'full-page.png', type: 'png', fullPage: true });
captureBeyondViewport controls whether content beyond the viewport can be captured; the documented default depends on whether a clip is supplied. Avoid combining it with an unverified clip while diagnosing. If you need one component, isolate it with an element screenshot:
const card = await page.waitForSelector('.report-card', { visible: true });
await card.screenshot({ path: 'report-card.png' });
If the element capture shows the expected pixels but a full-page capture does not, focus on page sizing, clipping, overlays, and full-page behavior. If both are blank, return to readiness, JavaScript, and failed-request checks. Consult ScreenshotOptions for documented options.
5. Distinguish a white background from missing content
A white image is not automatically an empty page. Many sites have a white background, so inspect known text or an element capture before concluding that rendering failed. omitBackground: true hides the default white background to allow transparency; it cannot restore content that never rendered. If you use it, verify the output format and downstream viewer support transparency.
// Use only when transparency is desired and content is known to render.
await page.screenshot({
path: 'transparent.png',
type: 'png',
omitBackground: true,
});
To separate page color from missing pixels, temporarily set a conspicuous background on the expected content container, or inspect its text and bounding box before capture. Do not rely on background transparency as a rendering fix.
6. Test deviceScaleFactor independently
Keep the same URL, viewport, wait condition, and screenshot options while changing only deviceScaleFactor. First compare scale 1 with the target scale. A Puppeteer issue documents a white-screenshot report associated with deviceScaleFactor: 2; it is a reason to test this axis, not evidence that scale is the cause of every blank capture. If scale 1 works and a higher value fails, preserve a minimal reproduction and record the Puppeteer version, Chromium environment, viewport, and page URL. See Puppeteer issue #3169.
for (const deviceScaleFactor of [1, 2]) {
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900, deviceScaleFactor });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.waitForSelector('main', { visible: true });
await page.screenshot({
path: `scale-${deviceScaleFactor}.png`,
type: 'png',
});
await page.close();
}
Do not change scale, viewport dimensions, full-page mode, and wait strategy simultaneously. One-variable comparisons make the failure reproducible and show whether it follows a setting or the page itself.
7. Check lazy-loaded content and page state
Below-the-fold images and sections may not exist until the page is scrolled. If a full-page capture omits them, reproduce with a viewport capture and inspect whether the target content loads after scrolling. Scroll in measured increments or trigger the application’s own load condition, then wait for the expected image or section before capturing. A very long page may also magnify memory and rendering costs, so isolate the target element when that meets the requirement.
Check authentication and state too. A redirect to a login page, expired session, consent gate, bot check, or API authorization failure can produce a valid screenshot of the wrong state. Log page.url(), response status, title, a short body-text sample, and relevant failed requests. Avoid printing secrets such as authorization headers or session cookies into production logs.
8. Troubleshooting reference
| Symptom | Likely cause | Next check or fix |
|---|---|---|
| Entire image is white; body text is empty | Navigation failed, app scripts did not run, or content was not ready. | Check goto() response, page.url(), page errors, failed requests, authentication, and a meaningful selector. |
| Shell appears but main content is blank | Client data request failed or capture ran before app state completed. | Inspect API responses and console errors; wait for a content selector or app readiness flag. |
| Only a small empty rectangle is captured | Clip coordinates or dimensions do not cover the intended content. | Remove clip; capture the viewport, then calculate and verify the rectangle. |
| Viewport works; full-page image is blank or wrong-sized | Full-page sizing, lazy loading, or page layout behavior differs from viewport capture. | Wait for content, test an element capture, trigger lazy loading, and add full-page mode only after the viewport works. |
| Scale 1 works; scale 2 fails | Scale-dependent browser or environment behavior. | Keep a minimal reproduction, record versions and dimensions, and test one scale at a time. |
| Transparent PNG looks blank in a viewer | The page background was omitted and the viewer shows transparency against white, or content itself is absent. | Repeat without omitBackground and verify rendered text or an element capture. |
| Capture sometimes works and sometimes does not | Timing race, transient network or API response, or variable page state. | Replace arbitrary sleeps with explicit readiness; log status and failed requests for each run. |
| Selector wait times out | Wrong selector, hidden element, wrong route, or an app that never reached the expected state. | Check URL and DOM, confirm selector spelling and visibility requirements, then identify the failed app dependency. |
9. A repeatable debugging checklist
- Set width, height, and
deviceScaleFactor: 1before navigation. - Navigate with a bounded timeout and record the response status and final URL.
- Wait for a visible element that means the content is ready.
- Log console errors, page errors, failed requests, and HTTP error responses.
- Capture a plain PNG without clip, transparency, or full-page options.
- Compare a known element capture with the viewport capture.
- Test full-page mode, transparency, and device scale one setting at a time.
- Keep a minimal reproduction when the failure depends on a scale or environment.
10. Performance, reliability, and cost
Readiness conditions improve reliability by aligning capture with the page’s actual state. Network-idle waits can add latency or fail to settle on pages with persistent requests; selectors and app-owned flags are often more targeted. Conversely, a selector that appears before its content is populated is insufficient, so choose a condition tied to finished data rather than a generic wrapper.
Full-page images and higher device scale factors increase the amount of rendered pixel data and can increase capture time and memory use. Keep the viewport and scale as small as the deliverable allows, and capture one element when a full document is unnecessary. Reuse one-variable diagnostic runs to avoid paying the time cost of repeatedly changing several settings without learning which one matters.
Browser automation cost depends on where and how you run Chromium; Puppeteer itself does not define a universal per-screenshot price. For operational reliability, bound navigation and selector waits, close pages and browsers in cleanup paths, and record enough diagnostics to reproduce intermittent failures without leaking credentials. Do not treat a screenshot as valid merely because the API call returned bytes; validate the expected page state before saving or publishing it.
Or skip the browser setup
If the goal is a screenshot rather than debugging Chromium, ScreenshotNeo provides a website screenshot API and MCP server. Its one GET request can return PNG, JPEG, WebP, or PDF; the documented options include viewport and device presets, full-page and element capture, waits, custom CSS or JavaScript, and more. See the ScreenshotNeo API docs.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Can Puppeteer tell whether a screenshot is visually blank?
The browser APIs described here expose page state and capture controls, but this guide does not rely on an image-analysis API. Check expected text, a known selector, response status, and an element capture; add pixel validation in your own pipeline if blank-image detection must be automated.
Should I always use networkidle2?
No. It is a useful baseline for requests that settle. A page-specific selector or readiness flag is more directly tied to application content; use network idle as an additional wait only where it makes sense.
Does changing the background fix a white screenshot?
No. Background options alter how the background is represented. They do not make missing application content render.
What evidence should I include in a bug report?
Provide a minimal script, Puppeteer and runtime versions, browser environment, viewport and scale, final URL and status, the exact wait condition, relevant errors, and a reproducible target where you have permission to capture it.


