ScreenshotNeo

BlogHow-to

How to Fix a Blank Screenshot in Puppeteer

A blank Puppeteer screenshot can come from page readiness, a hidden or incorrect target, the viewport, or browser setup. Follow this diagnostic sequence to find the cause.

By the ScreenshotNeo team4 October 20269 min read

A blank Puppeteer screenshot does not point to one universal cause. First check what the page has rendered immediately before capture. Then check whether the expected content is ready and visible, whether you are capturing the right page or element at the right viewport, and whether navigation or the browser environment failed.

Puppeteer’s documented page capture method is Page.screenshot(). It also supports ElementHandle.screenshot() for one element. The examples below help isolate which part of your capture flow is failing. Replace the example URL and selector with your own. Puppeteer Screenshots guide.

1. Start with a reproducible diagnostic capture

Run a basic script that logs navigation and page errors, waits for a page-specific visible element, checks its dimensions and text, and saves a full-page capture. This helps distinguish an empty or unfinished page from a screenshot-target problem.

import puppeteer from 'puppeteer';

const url = process.env.TARGET_URL ?? 'https://example.com';
const readySelector = process.env.READY_SELECTOR ?? 'h1';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });

  page.on('console', message => {
    if (message.type() === 'error') console.error('Console error:', 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('Final URL:', page.url());
  console.log('HTTP status:', response?.status() ?? 'no response');

  const target = await page.waitForSelector(readySelector, {
    visible: true,
    timeout: 15000,
  });
  const details = await target.evaluate(element => {
    const style = getComputedStyle(element);
    const rect = element.getBoundingClientRect();
    return {
      text: element.textContent?.trim(),
      width: rect.width,
      height: rect.height,
      display: style.display,
      visibility: style.visibility,
      opacity: style.opacity,
    };
  });
  console.log('Ready target:', details);

  await page.screenshot({ path: 'page.png', fullPage: true });
  await target.screenshot({ path: 'target.png' });
} finally {
  await browser.close();
}

Run it with TARGET_URL and READY_SELECTOR set to the affected page and a selector that means the useful content is ready. For example, in a POSIX shell: TARGET_URL=https://your-site.example READY_SELECTOR='#app main' node capture.mjs. The sample uses networkidle2 as a navigation wait and a separate app-specific selector. Network idleness alone is not proof that a single-page app has finished rendering.

2. Find out what is blank

Compare the page screenshot with the element screenshot, and inspect the saved files rather than relying only on whether the script completed.

Observation Likely branch Next check
Both page and target captures are blank The page may be blank, still loading, on an unexpected route, or failing at runtime. Check final URL, response status, console errors, failed requests, and the application’s ready condition.
Page capture has content but the element capture is blank The selector may identify the wrong node, a hidden node, or a zero-size element. Log the matched element’s text, bounding box, and computed style; confirm the selector matches the intended instance.
Viewport screenshot is blank but full-page capture has content, or the reverse The content may be outside the viewport, or the capture scope may not match the intended output. Check scroll position, viewport dimensions, and whether fullPage is appropriate.
It works locally but not in deployment The runtime, browser installation, system dependencies, environment, or network access may differ. Compare browser and Puppeteer versions, launch output, installed dependencies, cache path, and requests from the deployed process.

3. Wait for the application’s real ready state

page.goto() can wait for a navigation lifecycle condition, but a modern app may render its useful view later. Wait for content that signals readiness for your application, such as the main page heading, a populated results container, or a known route-specific element.

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('#app main', { visible: true, timeout: 20000 });

// If the element exists before it is populated, wait for useful content too.
await page.waitForFunction(() => {
  const main = document.querySelector('#app main');
  return main && main.textContent.trim().length > 0;
}, { timeout: 20000 });

await page.screenshot({ path: 'page.png' });

Choose the wait that matches the page:

  • waitUntil: 'networkidle2' is the wait shown in Puppeteer’s screenshot guide. It may be unsuitable for pages that keep requests open or poll in the background.
  • waitForSelector(selector, { visible: true }) waits for the element to exist and not have display: none or visibility: hidden. It does not establish that the content is meaningful, fully painted, or free from zero dimensions. See the Page.waitForSelector API.
  • waitForFunction() can wait for an app-specific condition, such as nonempty text or a loaded-state attribute. Keep the condition tied to the page’s own readiness contract.
  • A short fixed delay can be a temporary diagnostic, but it is brittle as a general readiness strategy: slow runs may still be early, while fast runs wait unnecessarily.

If a selector wait times out, verify the selector against the final page and route. The page may have redirected, returned an error page, rendered inside a different frame, or failed before creating the target.

4. Verify the selector, visibility, and capture scope

A selector resolving successfully does not guarantee that it identifies the visible content you meant to capture. Log the matched element’s tag, text, dimensions, and computed styles. Check duplicate matches and whether the first match is a hidden responsive variant.

const matches = await page.$$('main');
console.log('main matches:', matches.length);
for (const [index, element] of matches.entries()) {
  console.log(index, await element.evaluate(node => {
    const rect = node.getBoundingClientRect();
    const style = getComputedStyle(node);
    return {
      tag: node.tagName,
      text: node.textContent?.trim().slice(0, 120),
      x: rect.x,
      y: rect.y,
      width: rect.width,
      height: rect.height,
      display: style.display,
      visibility: style.visibility,
      opacity: style.opacity,
    };
  }));
}

const intended = await page.waitForSelector('#report', { visible: true });
await intended.screenshot({ path: 'report.png' });

Puppeteer documents that ElementHandle.screenshot() attempts to scroll an element into view if needed. That behavior can help when the right element is outside the viewport, but it cannot correct a selector that points to the wrong node or an element with no rendered content. For comparison, capture both the full page and the element as in the first example. Screenshot guide: page and element capture.

5. Set the viewport before navigation

Responsive sites can render different layouts at different viewport sizes. Set the viewport before navigating when the page responds to device size, then check that the target is in the intended layout and capture area. Puppeteer documents that viewport settings belong to each page and can affect site behavior. Page API.

const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle2' });
console.log('Viewport:', page.viewport());
await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'full.png', fullPage: true });

Check whether the desired content is below the fold, whether a mobile breakpoint hides or replaces it, and whether the requested full-page capture is necessary. A viewport screenshot and a full-page screenshot answer different questions; compare them during diagnosis.

6. Inspect navigation, JavaScript, and browser setup

If the browser page itself is blank, inspect the outcome of navigation before changing screenshot options. Log the final URL and response status. Listen for browser console errors, uncaught page errors, and failed requests. Then open the same URL in the installed browser environment if possible and check whether the app renders there.

  • Unexpected URL or response: redirects, access controls, or server errors can send the browser somewhere other than the expected app view.
  • Console or page errors: fix the application exception or environment-specific configuration that prevents rendering.
  • Failed requests: inspect blocked APIs, assets, DNS, authentication, and network restrictions. A shell process may not have the same access as your desktop browser.
  • Browser installation or launch problems: confirm that the Puppeteer-managed browser is installed and available in the process’s cache directory. The official troubleshooting guide covers missing browser downloads and cache configuration. Puppeteer troubleshooting.
  • Missing fonts: Puppeteer’s troubleshooting guide calls out additional font requirements for rendering some character sets, including Chinese, Japanese, and Korean. Missing fonts can affect text rendering; that guidance does not establish fonts as a general cause of a wholly blank page.

Do not add launch flags as a first response to a blank image. First establish whether navigation succeeded and whether the DOM contains visible, sized content. If the issue happens only in a container or serverless environment, compare its browser installation, system packages, user permissions, cache location, and network access with the working environment.

7. Common blank-screenshot errors and fixes

Symptom Common cause What to do
Screenshot is all white, but the script exits normally Capture happened before app content appeared, or the app rendered an empty/error route. Log URL and response; wait for a page-specific visible selector and meaningful content; inspect runtime errors.
waitForSelector times out Wrong selector, wrong route, delayed content, or selector exists only in a frame. Inspect the final URL and DOM, verify the selector, and check the relevant frame. Increase the timeout only if the page genuinely needs more time.
Selector resolves but element image is empty Hidden responsive duplicate, zero-size node, transparent/empty content, or wrong matched element. Enumerate matches and inspect text, dimensions, and styles; use a more specific selector.
Screenshot differs across runs Application readiness or external resources vary, or the viewport differs. Use an application-specific readiness condition, set a fixed viewport before navigation, and log failed requests.
Works on a laptop but fails in CI or production Browser binary, cache path, system dependencies, permissions, fonts, or network differ. Check Puppeteer’s browser installation and environment using the official troubleshooting guide; reproduce inside the deployed image.
Screenshot API returns a Uint8Array but saved file appears blank The capture may be valid bytes for a blank page, or the output handling/path may be wrong. Inspect the page state before capture and open the generated file; confirm the intended path and that the bytes are written without conversion errors.

8. Make screenshot jobs more reliable and efficient

  • Wait for the smallest meaningful condition. A page-specific selector or readiness flag is usually more informative than waiting for every network connection to settle.
  • Capture only what you need. Use an element capture for a component and a page or full-page capture for the whole document. Large pages take more resources to render and encode.
  • Fix viewport and inputs. Stable viewport dimensions, URL, authentication, and page state make failures easier to reproduce.
  • Keep diagnostics on failures. Record final URL, response status, console and page errors, failed requests, selector details, and browser/Puppeteer versions. Avoid logging secrets such as authorization headers or cookies.
  • Close pages and browsers. Use try/finally so failures do not leave browser processes running and consuming memory.
  • Set timeouts intentionally. Navigation and readiness waits should match your service’s allowed job duration. An infinite wait can tie up workers; an overly short timeout can turn normal slow loads into false failures.

Puppeteer’s screenshot API returns image data as a Promise (a Uint8Array by default, or base64 when that encoding is selected). See the Page.screenshot API when you need to handle the bytes rather than save directly to a path.

Or skip the browser setup

If your goal is a reliable website image rather than debugging a Puppeteer runtime, ScreenshotNeo provides a one-request screenshot API. 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}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo removes cookie banners, 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. Learn about ScreenshotNeo, or sign up for 1,000 free screenshots a month with no card.

FAQ

Does a blank screenshot mean Puppeteer failed to save the file?

No. The browser can successfully save an image of a page that is itself empty or not ready. Check the rendered page state and output file separately.

Should I always use networkidle2?

No. It is used in Puppeteer’s screenshot example, but the right wait depends on the page. Prefer an application-specific readiness condition when one is available.

What information is useful when asking for help with a specific blank capture?

Share a minimal script, the target page if public, Puppeteer and browser versions, whether page or element capture is blank, the final URL and status, and relevant console or failed-request logs. Remove credentials and private cookies first.

Sources