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.

Start the navigation wait before clicking the link, and await the click and wait together. For a conventional document navigation, use page.waitForNavigation({ waitUntil: 'load' }). Then wait for the destination state your script actually needs, such as a specific element. This avoids the click/navigation race and prevents treating the browser’s load event as proof that an application has finished rendering.

The reliable click-and-wait pattern

Puppeteer’s documented pattern is to create the navigation promise before triggering the click:

const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'load' }),
  page.locator('a.some-link').click(),
]);

The order matters. A click can start navigation immediately; if your code calls waitForNavigation() afterward, the wait may miss the event and hang until its timeout. Puppeteer’s Page API explicitly recommends awaiting the two operations together.

The returned response is the navigation response for a normal document navigation. Do not assume it is always non-null: same-document route changes made with the History API, and anchor jumps, can resolve the wait with null. In those cases, verify the URL or destination content instead. See Page.waitForNavigation().

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

A complete runnable example

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

try {
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  const [response] = await Promise.all([
    page.waitForNavigation({
      waitUntil: 'load',
      timeout: 30_000,
    }),
    page.locator('a.some-link').click(),
  ]);

  // For an ordinary document navigation, response contains the response object.
  // For History API or anchor navigation, it can be null.
  console.log('URL after click:', page.url());
  console.log('Navigation response:', response ? response.status() : 'same-document');

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

  console.log('Destination-specific content is ready.');
} finally {
  await browser.close();
}

Replace the URL, link selector, and readiness selector with values from your application. The selector wait is intentionally separate from the navigation wait: load describes a browser lifecycle milestone, while the selector describes the outcome your automation requires.

What “complete page load” should mean

Document navigation

waitUntil: 'load' waits for the page’s load lifecycle event. It is appropriate when the task is complete once the document and its load-blocking resources have reached that event. It does not guarantee that client-side rendering, API calls, lazy content, animations, or background work have finished.

Destination-specific readiness

For an application, wait for a meaningful element after the navigation promise resolves:

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

Page.waitForSelector() waits for a matching element and documents a 30-second default timeout. Set a timeout appropriate to your service and catch failures so a missing destination does not silently produce an invalid result.

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

Network inactivity

You can wait for network idleness when it represents a useful condition for your page:

await page.waitForNetworkIdle({
  idleTime: 1_000,
  concurrency: 0,
  timeout: 30_000,
});

Puppeteer 25.12.0 documents that waitForNetworkIdle() always waits at least the configured idle interval. Its options document a default idleTime of 500 milliseconds and a default concurrency of 0 in WaitForNetworkIdleOptions. A page with polling, analytics, WebSockets, or other persistent requests may never become idle in the way you expect. Network silence is a network condition, not a universal definition of application readiness; a page-specific selector is usually clearer.

Choosing the right wait strategy

Requirement Pattern Meaning
Traditional document navigation waitForNavigation({ waitUntil: 'load' }) before the click The navigation reached the selected lifecycle event.
DOM is parsed but subresources may continue waitForNavigation({ waitUntil: 'domcontentloaded' }) The DOMContentLoaded event fired; later resources or rendering may still be pending.
Application-specific state Navigation wait followed by waitForSelector() or a Locator assertion The destination exposes the condition your task needs.
Quiet network required waitForNetworkIdle() with explicit options Puppeteer observed the configured idle interval and request threshold.
Anchor or History API route change Navigation wait plus URL/content verification The route or content changed; the navigation response may be null.

Make the click reliable

Puppeteer’s current interaction guide recommends Locators for selecting and interacting with elements. A Locator click checks that the element is in the viewport, visible, enabled, and stable across consecutive animation frames. Those checks make the click itself more dependable, but they do not wait for the navigation that follows it. Start the navigation wait separately as shown above. See Puppeteer page interactions.

When a selector is not enough

  • Use a selector that identifies the intended link uniquely, such as a[data-testid="account-link"].
  • Wait for the link to exist before creating the click/navigation pair if the page renders it asynchronously.
  • Scroll or close an obstructing modal before clicking; Locator preconditions do not remove unrelated overlays.
  • For links that open another target, treat the new page as a separate target and wait for that page’s navigation. The reviewed API material does not provide a single version-verified recipe for every popup scenario, so confirm the target-handling API for your installed Puppeteer version.

Same-document navigation and route checks

Single-page applications often call history.pushState() or history.replaceState() instead of requesting a new document. An anchor may only scroll the current page. In both cases, waitForNavigation() can resolve with null. Do not write code that unconditionally calls response.status(). Check the route and then wait for destination content:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'load', timeout: 30_000 }),
  page.locator('a[data-testid="reports"]').click(),
]);

if (!page.url().includes('/reports')) {
  throw new Error(`Unexpected destination: ${page.url()}`);
}

await page.waitForSelector('[data-testid="reports-view"]', {
  visible: true,
  timeout: 30_000,
});

console.log(response ? `HTTP ${response.status()}` : 'Same-document route change');

Timeouts, errors, and diagnosis

“Navigation timeout exceeded”

Common causes include a click that did not trigger navigation, a slow server, a blocked request, or a persistent application that never reaches the selected lifecycle event. Confirm the selector, inspect page.url(), and choose the correct wait condition. If the page changes in place, do not wait for a document response; wait for its route or readiness selector.

The script misses the navigation

This is usually the race caused by clicking first and calling waitForNavigation() second. Put both operations in the same Promise.all, with the wait expression listed first.

The wait resolves but content is incomplete

The load event fired before client-side data or lazy rendering finished. Add a selector representing the final state. If no stable selector exists, ask the application team for a test identifier rather than relying on arbitrary sleeps.

Network-idle wait never finishes

Polling, analytics, streaming connections, or continuously refreshed resources can keep requests active. Reduce reliance on global network silence and wait for the specific UI state. If network idleness is genuinely the requirement, set an explicit idleTime, concurrency, and timeout and log which condition failed.

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

Selector timeout

The destination may be wrong, the element may be hidden, the selector may have changed, or the application may have displayed an error state. Capture the URL, page HTML or screenshot, and console/network errors at the timeout. The documented default selector timeout is 30 seconds; setting it explicitly makes behavior predictable.

Click is rejected

Locators reject clicks when the target is not visible, enabled, in the viewport, or stable. Wait for the element, dismiss overlays, and ensure the page is at the expected route. Avoid forcing a click unless you understand why the normal interaction preconditions cannot be satisfied.

Performance and reliability practices

  • Use the narrowest readiness condition that proves the task is complete; waiting for unrelated background requests increases latency.
  • Use a stable data-testid or similarly controlled selector instead of a presentation class.
  • Set explicit timeouts at the navigation and destination-state boundaries, then report which boundary failed.
  • Log the URL, navigation response status when available, elapsed time, and final readiness condition.
  • Keep lifecycle waiting and application-state waiting as separate steps so failures are diagnosable.
  • Confirm the API signatures and defaults against the Puppeteer version installed in your project. The referenced pages identify the current API reference as Puppeteer 25.12.0, and defaults can change.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is simply to obtain a finished page image or PDF after a link-driven destination is ready, ScreenshotNeo provides a website screenshot API and MCP server. Its capture options include waiting for a selector, a delay, or network idle, so you can express the destination condition without maintaining Puppeteer infrastructure.

For example, request a screenshot of the destination URL with cURL:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
The SQL Programming Language: .
  • Used Book in Good Condition
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for the full parameter set. You can also use the supplied Python or Node.js clients:

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}`);

Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots, and response headers identify the page verdict and billing result. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Should I use load or networkidle?

Use the condition that matches the task. Neither event alone proves that application-specific rendering is complete; add a destination selector when that state matters.

Can I call waitForNavigation() after the click?

Do not. Register it before the click and await both operations with Promise.all to avoid the race.

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

Why is the navigation response sometimes null?

History API route changes and anchor navigation can be same-document operations, so there may be no new HTTP response.

What is the default selector timeout?

Puppeteer’s documented default for waitForSelector() is 30 seconds; set it explicitly when a different budget is appropriate.

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.