ScreenshotNeo

BlogHow-to

Why Does Puppeteer Capture a Blank Page Screenshot?

A blank Puppeteer screenshot is a symptom, not a diagnosis. Check the final URL, response status, rendered content, and frames before capturing.

By the ScreenshotNeo team4 October 20267 min read

A blank Puppeteer screenshot usually means the browser captured a page before the expected content was present, navigated somewhere other than the intended document, or rendered the content in a frame you did not inspect. Start by checking the final URL and the response from page.goto(); then wait for the specific content you need and capture it.

A resolved navigation does not prove that the right page or content loaded. In particular, page.goto() can return null for about:blank or same-document hash navigation, and headless shell does not necessarily throw for HTTP 404 or 500 responses. Puppeteer’s Page.goto() reference documents these cases.

1. Diagnose the page before capturing

  1. Record the requested and final URL. Log the URL you pass to goto() and page.url() afterward. Check redirects and URL construction. If the final URL is about:blank, the screenshot may be accurately capturing a blank document.
  2. Inspect the main response. Keep the value returned by goto(). If it exists, log its status and URL. A 404 or 500 can produce an HTTP response without making navigation throw in headless shell.
  3. Check for the content you expect. Test for a page-specific selector or text, and wait for it if the page renders content in client-side JavaScript. A generic navigation milestone is not a guarantee that your application has finished rendering.
  4. Check frames. If the content belongs to an iframe, list the page’s frames and query the frame containing the content.
  5. Compare page and element captures. If the page screenshot looks blank, try capturing the expected element directly. This helps distinguish a missing page from a missing or misplaced region.

Puppeteer’s screenshot guide demonstrates both Page.screenshot() and ElementHandle.screenshot(). Its Page API reference covers selector and function waits and frame inspection.

2. Use a content-based wait and capture

This runnable Node.js example logs the navigation result, waits for a selector that represents the content you want, and saves a full-page screenshot. Replace the URL and selector with values from your page.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    const targetUrl = 'https://example.com';
    const expectedSelector = 'main h1';

    const response = await page.goto(targetUrl, {
      waitUntil: 'domcontentloaded',
      timeout: 30_000,
    });

    console.log({
      requestedUrl: targetUrl,
      finalUrl: page.url(),
      status: response ? response.status() : null,
      responseUrl: response ? response.url() : null,
    });

    if (response && !response.ok()) {
      throw new Error(`Main document returned HTTP ${response.status()}`);
    }

    await page.waitForSelector(expectedSelector, {
      visible: true,
      timeout: 15_000,
    });

    const heading = await page.$eval(
      expectedSelector,
      element => element.textContent.trim()
    );
    console.log('Expected content:', heading);

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

    const element = await page.$(expectedSelector);
    await element.screenshot({ path: 'expected-element.png' });
  } catch (error) {
    console.error('Capture failed:', error);
    process.exitCode = 1;
  } finally {
    await browser.close();
  }
})();

Install Puppeteer with npm install puppeteer and run the file with Node.js. The status check is useful when an HTTP error page is not the content you want. Remove or change it if the site intentionally serves the target content with a non-2xx status. The selector is an example; use a stable element that appears only when the required content is ready.

3. Choose the right readiness condition

waitUntil controls which navigation event Puppeteer waits for. It does not establish that a particular client-rendered component is ready. Choose a navigation event that suits the page, then wait for the content that matters.

Wait condition What it establishes When to use it
domcontentloaded The document has been parsed and the DOM content loaded event fired. A useful starting point when you will follow navigation with a selector or application-ready wait.
load The load event fired after page resources that block that event have loaded. Pages whose required content depends on ordinary load-event resources.
networkidle0 The network has had no active connections for the configured idle window. Pages that settle their relevant network activity; can be unsuitable for pages with persistent connections or background polling.
networkidle2 The network has had at most two active connections for the idle window. Some pages with a small amount of continuing network activity. It still does not prove a component is ready.

For a page-specific condition, use waitForSelector() or waitForFunction(). For example, if an application sets a global readiness flag after rendering, wait for that flag:

await page.waitForFunction(
  () => window.appReady === true,
  { timeout: 15_000 }
);
await page.screenshot({ path: 'ready.png' });

Replace window.appReady with a signal your application actually sets. A selector is often simpler and ties the wait directly to visible page content. Avoid adding arbitrary sleep delays as the only readiness check: they can be too short on slow runs and unnecessarily long on fast ones.

4. Check content inside an iframe

A selector queried on the top-level page cannot find elements inside a separate frame. Inspect the frames after navigation, identify the frame by its URL or another property, and wait within that frame:

for (const frame of page.frames()) {
  console.log({ url: frame.url(), name: frame.name() });
}

const frame = page.frames().find(frame => frame.url().includes('/embedded-content'));
if (!frame) {
  throw new Error('Expected content frame was not found');
}

await frame.waitForSelector('.report-ready', {
  visible: true,
  timeout: 15_000,
});
const text = await frame.$eval('.report-ready', el => el.textContent.trim());
console.log(text);
await page.screenshot({ path: 'page-with-frame.png', fullPage: true });

Use a frame URL condition that matches your application, not the illustrative path above. If the frame is attached after the initial navigation, inspect frames again after the parent page’s content becomes ready.

5. Common causes and fixes

Symptom Likely explanation What to check or change
Screenshot is completely white and final URL is about:blank. The script captured the initial blank page or navigated to an empty destination. Check URL construction, the navigation call, redirects, and page.url() immediately before capture.
goto() resolved, but the intended page is missing. A resolved promise does not establish that the main response was successful; an HTTP error response may still be returned. Inspect response.status(), response.url(), and the visible page content.
Navigation finished, but the app area is empty. Client-side rendering or data loading may not have completed. Wait for the specific visible selector or an application readiness condition; inspect console and page errors if it never appears.
The page has content but the expected element is absent. The element may be in an iframe or the selector may not match the rendered page. List frames, query the correct frame, and verify the selector against the actual DOM.
The element capture works, but the full-page result appears empty or unexpected. The expected region rendered, so investigate the full-page capture or page layout separately. Compare viewport and full-page captures; inspect element visibility, layout, and screenshot options.
waitForSelector times out. The selector never appeared, is incorrect, belongs to a frame, or the page failed before rendering it. Log the final URL and response status, inspect the selector in the correct document or frame, and check page errors.
goto() times out. The navigation did not reach the chosen event within the timeout, or the page keeps network activity open. Use an appropriate navigation event, set a considered timeout, and wait separately for the actual content condition.

When the checks do not identify the cause, collect the requested and final URL, Puppeteer and Chromium versions, launch and headless configuration, response status, console messages, page errors, frame URLs, and the relevant navigation, wait, and capture code. Without those details there is no universal root cause to assume.

6. Reliability, performance, and cost

  • Reliability: Make readiness explicit with a selector or application signal. Check final URL and response status on every capture, and fail with logs that identify which check failed.
  • Performance: Wait for the smallest condition that represents the content you need. Waiting for every network request can delay captures on pages with analytics, polling, or persistent connections. Full-page screenshots can also take more work than a viewport capture, so capture only the needed region when appropriate.
  • Timeouts: Set navigation and content-wait timeouts to match the page and environment. Report which operation timed out; increasing every timeout can hide a page that never becomes ready.
  • Cost: A self-hosted Puppeteer flow has no per-screenshot API charge, but it uses compute, browser memory, maintenance, and engineering time. Account for concurrency and browser cleanup in a capture service.

Or skip the browser setup

ScreenshotNeo accepts one GET request with a URL and returns an image or PDF. Its API documentation describes the available parameters.

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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed. Response headers report the page verdict and billing status.
  • An MCP server lets AI agents, including Claude and Cursor, take screenshots.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000; every feature is on every plan.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

FAQ

Does a successful page.goto() mean the page loaded correctly?

No. Check the final URL and, when a response exists, its status. Then verify the content your screenshot requires.

Should I always wait for network idle?

No. Network idle describes network activity, not whether a particular component rendered. Prefer a page-specific selector or readiness condition when you can identify one.

How can I tell whether the missing content is in an iframe?

Inspect page.frames() and check each frame’s URL and name. Query and wait for the content in the matching frame.

Can I capture only the element I need?

Yes. Find it with a selector and call ElementHandle.screenshot(). This is also a useful diagnostic comparison with a full-page capture.

Sources