Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for navigation to reach an explicit boundary, then wait for the page state your image actually needs, and only then call page.screenshot(). For many pages, waitUntil: 'networkidle2' is a practical starting point; dynamic applications often need page.waitForSelector(), page.waitForFunction(), or a short post-navigation network-idle wait as well.

The reliable Puppeteer sequence

This complete example launches Chromium, opens a URL, waits for most network activity to settle, waits for a page-specific readiness marker, captures a full-page PNG, and closes the browser even when an error occurs.

import puppeteer from 'puppeteer';

const url = 'https://example.com';
const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.goto(url, {
    waitUntil: 'networkidle2',
    timeout: 30_000,
  });

  // Use this only when the site exposes a real visual-readiness marker.
  // await page.waitForSelector('[data-testid="report-ready"]', {
  //   visible: true,
  //   timeout: 15_000,
  // });

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

page.goto() resolves at the navigation boundary selected by waitUntil. The screenshot guide’s standard pattern uses networkidle2 followed by page.screenshot(). Always await the screenshot promise; otherwise your process can exit before the file is written.

Choose the right navigation boundary

The waitUntil value describes a browser event or network condition, not a guarantee that every visual detail is finished.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Boundary What it observes Use it when Important limitation
domcontentloaded The initial HTML has been parsed. Your shot depends on the DOM structure but not on images, fonts, or later resources. Images, web fonts, and client-rendered content may still be missing.
load The page’s load event has fired. The site’s load event is a suitable readiness boundary. JavaScript may continue rendering after the event.
networkidle2 Network activity has settled to no more than two active requests for the required idle period. You want a general screenshot boundary and the page does not maintain many long-lived requests. It can still resolve before a particular component is visually ready.
networkidle0 There are no active network requests for the required idle period. Zero in-flight requests is realistic for the target. Analytics, polling, streaming, or WebSocket connections can prevent it from resolving.

For a conventional document, start with networkidle2. If it times out because the page polls or streams data, move to domcontentloaded or load and add a condition tied to the content you need.

Wait for resources that arrive after navigation

A navigation can finish while a single-page application is still fetching data. In that case, perform a separate network-idle wait after goto():

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/dashboard', {
    waitUntil: 'domcontentloaded',
    timeout: 30_000,
  });
  await page.waitForNetworkIdle({
    idleTime: 500,
    timeout: 10_000,
  });
  await page.screenshot({ path: 'dashboard.png' });
} finally {
  await browser.close();
}

page.waitForNetworkIdle() resolves after the configured idle period and always waits at least that long. It is a traffic measurement, however, so it does not prove that the exact chart, image, or text you want is present.

Wait for the exact visual state

Wait for a visible selector

If the application adds a reliable marker when rendering finishes, wait for that marker instead of guessing with a delay:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.goto('https://example.com/report', {
  waitUntil: 'domcontentloaded',
  timeout: 30_000,
});

await page.waitForSelector('[data-testid="report-ready"]', {
  visible: true,
  timeout: 15_000,
});

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

The selector should represent the state that the screenshot must contain, such as a rendered table or a “loaded” marker. A selector for a permanent shell element is not sufficient.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Wait for an application predicate

When readiness is stored in a JavaScript property, use waitForFunction():

await page.goto('https://example.com/app', {
  waitUntil: 'domcontentloaded',
  timeout: 30_000,
});

await page.waitForFunction(
  () => document.querySelectorAll('.result-row').length >= 20,
  { timeout: 15_000 },
);

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

Keep the predicate specific and bounded. If the condition can never become true, Puppeteer should fail with a timeout rather than silently creating an incomplete image.

Use a bounded delay only as a fallback

A fixed delay can accommodate an animation or a third-party widget when no selector or predicate exists, but it is a timing guess. Prefer a page-specific condition and retain the delay only when the visual transition itself is the requirement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Synchronize clicks that trigger navigation

For links, form submissions, or buttons that navigate, begin waiting before the action. Starting afterward can miss a fast navigation:

await Promise.all([
  page.waitForNavigation({
    waitUntil: 'networkidle2',
    timeout: 30_000,
  }),
  page.click('a.next'),
]);

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

If the click updates the current document without navigation, do not use waitForNavigation(); wait for the selector, predicate, or network condition produced by that update instead.

A production-ready helper

Centralizing navigation, readiness, timeout handling, and diagnostics makes batch captures easier to operate:

import puppeteer from 'puppeteer';

async function capture(url, outputPath) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  page.setDefaultTimeout(15_000);

  try {
    const response = await page.goto(url, {
      waitUntil: 'networkidle2',
      timeout: 30_000,
    });

    if (!response) {
      throw new Error(`No main-resource response for ${url}`);
    }

    await page.screenshot({
      path: outputPath,
      fullPage: true,
    });
  } catch (error) {
    await page.screenshot({ path: `${outputPath}.error.png` }).catch(() => {});
    throw error;
  } finally {
    await browser.close();
  }
}

await capture('https://example.com', 'example.png');

The diagnostic image is useful when a navigation or readiness wait fails. In a worker, record the URL, wait mode, timeout, and error message with the job so an incomplete capture is distinguishable from a successful one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why screenshots still miss images or text

Lazy-loaded content is below the initial viewport

Some sites request images only after an element approaches the viewport. A full-page screenshot does not guarantee that every lazy image has been requested before capture. Wait for a site-provided “all content loaded” marker, scroll through the page to trigger lazy loading, or capture after the relevant image elements report completion.

Network idle does not equal visual idle

Fonts can swap after the network quiets, a client-side render can occur in a microtask, and a CSS animation can still be moving. A selector or predicate that represents the finished state is more precise than a global network condition.

The page never becomes idle

Polling, analytics, streaming, and WebSockets can keep requests active indefinitely. Use domcontentloaded or load, then wait for the component you need with a finite timeout. Do not increase a timeout forever to compensate for a condition that is structurally impossible.

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

A cookie banner or modal covers the page

This is a page-state issue, not a Puppeteer wait issue. Accept or dismiss the banner in your script, or hide the relevant element before capture. If the banner is produced by an external consent platform, its timing can vary between runs, so use a bounded selector wait and a fallback path.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting common failures

Symptom Likely cause Fix
Navigation timeout of 30000 ms exceeded The server is slow, the URL is unreachable, or the selected network-idle condition never occurs. Verify the URL, increase the timeout for this site, or use domcontentloaded/load plus an application-specific wait.
Waiting for selector ... failed The selector is wrong, the element is inside a frame, or the application never reaches that state. Inspect the rendered DOM, target the correct frame, confirm the state transition, and keep a finite timeout.
The screenshot is mostly blank The capture ran before client rendering, or a script failed. Capture an error artifact, inspect console/page errors, and wait for a real readiness marker rather than adding an arbitrary long delay.
Images are absent Lazy loading, blocked requests, failed image URLs, or a font/image request still in progress. Trigger lazy loading, verify image completion, check request failures, and choose a wait condition that covers the required resources.
waitForNavigation() hangs after a click The click performs an in-page update instead of navigation. Replace it with waitForSelector(), waitForFunction(), or waitForNetworkIdle() for the update.
networkidle0 never resolves Persistent polling, streaming, analytics, or a WebSocket keeps a connection open. Use networkidle2 or an event-based boundary, then wait for the exact content required.

Performance, reliability, and cost considerations

Use the least expensive wait that is correct

domcontentloaded usually returns sooner than a network-idle condition, but speed is not a benefit if the resulting image is incomplete. For static pages, load or networkidle2 may be enough. For applications, a precise selector often avoids waiting for unrelated background requests.

Bound every asynchronous operation

Set navigation and readiness timeouts appropriate to the site. A timeout should produce an error and, where possible, a diagnostic screenshot or page log. Treat retries carefully: retry transient network failures, but do not retry a permanently false selector without changing the condition.

Keep browser lifecycle explicit

Reuse a browser process for a controlled batch when startup cost matters, but create isolated pages and close them after each job. Always close the browser in a finally block so failed captures do not leak Chromium processes.

Understand what Puppeteer does not promise

No single wait mode guarantees that every image, font, animation, advertisement, or client-rendered component is complete. Readiness is application-specific. The most reliable boundary is the one that corresponds to the visual state your user needs.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a one-request screenshot API when you do not want to maintain Chromium, navigation waits, and page cleanup. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether it was billed. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.

Use the API documentation at https://screenshotneo.com/docs/ for authentication and options. This cURL request saves a WebP image:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

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)

Node.js:

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 failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo includes full-page and element capture, dark mode, device presets, custom viewports and retina scale, PDF output, HTML/CSS rendering, custom JavaScript and CSS, clicks, selector waits, delays, network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous webhooks, bulk capture for up to 100 URLs per call, a usage API, an OpenAPI specification, and compatibility with parameter names used by other screenshot APIs. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can I combine more than one readiness condition?

Yes. A common pattern is navigation with domcontentloaded, followed by waitForNetworkIdle(), followed by a selector or predicate. Each condition should serve a distinct purpose and have its own timeout.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What does a null response from page.goto() mean?

The navigation can resolve without a main-resource response for certain navigation types. If your workflow requires an HTTP response, check for a null value and handle it as a diagnostic or application error.

Should I wait before every screenshot?

Wait whenever navigation or an interaction can change the page. If several screenshots are taken from the same already-ready state, capture them without repeating a global wait, while still synchronizing any action that changes the content.

Frequently Asked Questions

Can I combine more than one readiness condition?

Yes. A common pattern is navigation with domcontentloaded, followed by waitForNetworkIdle(), followed by a selector or predicate. Each condition should serve a distinct purpose and have its own timeout.

What does a null response from page.goto() mean?

The navigation can resolve without a main-resource response for certain navigation types. If your workflow requires an HTTP response, check for a null value and handle it as a diagnostic or application error.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I wait before every screenshot?

Wait whenever navigation or an interaction can change the page. If several screenshots are taken from the same already-ready state, capture them without repeating a global wait, while still synchronizing any action that changes the content.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.