Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use two waits, not one: wait for navigation to reach a sensible lifecycle point, then wait for a page-specific signal that the JavaScript-rendered content you need actually exists. Puppeteer’s networkidle0 and networkidle2 describe network activity, not application readiness. A stable selector or a waitForFunction() assertion is usually the final gate before reading the DOM or taking a screenshot.
The reliable pattern
This example navigates, waits for the document to be parsed, waits for an application-owned readiness marker, extracts text in the page context, and captures the rendered result. Replace the selector and condition with signals that the target site controls.
const puppeteer = require('puppeteer');
const url = 'https://example.com/app';
(async () => {
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
await page.goto(url, {waitUntil: 'domcontentloaded', timeout: 60_000});
// Prefer a stable, meaningful application signal.
await page.waitForSelector('[data-ready="true"]', {timeout: 30_000});
const result = await page.evaluate(() => {
return document.querySelector('#result')?.textContent?.trim() ?? '';
});
if (!result) throw new Error('Expected #result to contain text');
await page.screenshot({path: 'rendered.png', fullPage: true});
console.log(result);
} finally {
await browser.close();
}
})();
domcontentloaded only means the initial HTML has been parsed. The selector wait is what ties completion to the content your job needs. If the page has no stable marker, use a function wait that checks a specific state, such as a non-empty result, a loading element disappearing, or a known application flag.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →What each readiness check proves
| Strategy | What it establishes | Strength | Main risk |
|---|---|---|---|
| Navigation lifecycle | The navigation reached the selected browser lifecycle event. | Simple control over goto(). |
Client-side rendering may still be running. |
| Network idle | Requests met an idle threshold for the configured period. | Useful when the page loads data in a finite burst. | Polling, analytics, sockets, or ads can prevent idle; idle does not prove the desired component rendered. |
| Selector wait | A matching DOM element exists (and, with options, meets visibility requirements). | Directly tied to visible application structure. | A stale shell element can appear before its text or data is ready. |
| Function wait | Your predicate became true in the page. | Can test text, attributes, counts, or application state. | Requires a condition that cannot remain false forever. |
| Fixed delay | Only that a specified amount of time elapsed. | Works as a last resort for pages with no observable signal. | It can be too short or wastefully long and does not assert success. |
Puppeteer’s current API documentation describes waitForNetworkIdle() as waiting for the network to be idle. Its documented defaults are an idle time of 500 ms and concurrency of 0, and the wait lasts at least the configured idle time. Treat that as a network checkpoint, not a guarantee that a framework has finished hydrating.
#1 Best Overall
Choosing networkidle0 or networkidle2
The official screenshot guide demonstrates waitUntil: 'networkidle2' before taking a screenshot. The names indicate different allowed in-flight-request thresholds: networkidle0 waits for no active connections, while networkidle2 permits up to two. A continuously connected page can make networkidle0 time out; networkidle2 is often more tolerant of background activity.
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 60_000
});
await page.waitForSelector('.report-row', {timeout: 30_000});
For more explicit control, use the network-idle API with its options, then still assert the page-specific state:
await page.waitForNetworkIdle({idleTime: 1_000, concurrency: 2, timeout: 30_000});
await page.waitForFunction(() => {
const el = document.querySelector('#result');
return !!el && el.textContent.trim().length > 0;
}, {timeout: 30_000});
Waiting for the exact content you need
Use a stable selector
Prefer selectors based on data attributes or semantic application elements rather than generated class names. If the element exists immediately but is populated later, wait for its content instead of its presence.
Rank #2
await page.waitForFunction(() => {
const status = document.querySelector('[data-status]');
return status?.getAttribute('data-status') === 'complete';
}, {timeout: 30_000});
Wait for a loading state to end
await page.waitForSelector('.loading', {hidden: true, timeout: 30_000});
await page.waitForSelector('.results', {timeout: 30_000});
Pass values into evaluate()
Page.evaluate() serializes the function and runs it inside the browser page. It cannot see local Node.js variables or helper functions unless you pass values as arguments or define the logic inside the evaluated function.
const selector = '#result';
const text = await page.evaluate((sel) => {
return document.querySelector(sel)?.textContent?.trim() ?? null;
}, selector);
Return serializable values such as strings, numbers, arrays, and plain objects. For a DOM object that must remain referenced, use evaluateHandle() instead of expecting a normal return value to preserve the node.
When a click or submit causes navigation
Start the navigation wait before the action and await both promises together. Starting the wait afterward can miss a fast navigation.
const [response] = await Promise.all([
page.waitForNavigation({waitUntil: 'domcontentloaded', timeout: 60_000}),
page.click('button[type="submit"]')
]);
console.log('status:', response?.status(), 'url:', page.url());
await page.waitForSelector('#results');
waitForNavigation() resolves to the main-resource response for an ordinary navigation. Same-page hash changes and History API transitions can resolve to null, so inspect page.url() and then wait for the in-page state change.
Recommended Free Tools
Make sure JavaScript is actually enabled
Check the setting before diagnosing an empty page:
console.log('JavaScript enabled:', await page.isJavaScriptEnabled());
If you changed it with setJavaScriptEnabled(), navigate again. The documented behavior takes full effect on the next navigation; it does not undo scripts that already ran.
await page.setJavaScriptEnabled(true);
await page.goto(url, {waitUntil: 'domcontentloaded'});
await page.waitForSelector('[data-ready="true"]');
Inspect what failed instead of guessing
Confirm the destination and response
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 60_000
});
console.log({status: response?.status(), finalUrl: page.url()});
Redirects, login walls, and alternate hostnames become visible in the final URL and status. A navigation response alone does not explain a script failure, so collect page-specific evidence.
Rank #4
Capture console and page errors
page.on('console', msg => console.log('[browser]', msg.type(), msg.text()));
page.on('pageerror', err => console.error('[pageerror]', err));
page.on('requestfailed', req => console.error('[requestfailed]', req.url(), req.failure()?.errorText));
These events can distinguish an application exception or failed request from a merely premature read. They do not, by themselves, prove why an external site failed; investigate the URL’s authentication, challenge, and resource behavior.
Common symptoms and fixes
“Puppeteer returns an empty page”
- Check
page.url()and the navigation status for redirects or an unexpected destination. - Verify JavaScript is enabled, then navigate again after changing the setting.
- Replace a short delay with
waitForSelector()orwaitForFunction()for the actual result. - Listen for console, page-error, and failed-request events before changing browser flags.
Network-idle timeout
Background polling, long-lived connections, or third-party resources may keep the threshold from being met. Try the less strict networkidle2, configure waitForNetworkIdle() deliberately, or skip network idle and wait for the result selector.
The selector times out
- Inspect the final URL and page HTML to confirm you are on the expected route.
- Check whether the element is inside an iframe; a frame requires its own selector wait.
- Confirm the selector is stable and that the application actually renders that state for this user.
- Use a function wait for text or an attribute when the element is present before its data arrives.
A click changes the URL but the wait returns null
That can be a same-document History API transition rather than a full navigation. Wait for the resulting application marker or URL change instead of requiring a response object.
Best Value
- Used Book in Good Condition
Performance and reliability practices
- Set explicit navigation and content timeouts so a broken page cannot occupy a worker indefinitely.
- Use one browser process with separate pages when appropriate, but always close pages and the browser in
finallyblocks. - Wait for the smallest meaningful condition. A page-specific predicate usually finishes sooner than an idle rule on a site with continuous background traffic.
- Keep screenshots and extracted data after the same readiness assertion; otherwise the image and text can represent different render states.
- Use a fixed delay only when no observable condition exists, and document why that delay is sufficient for your page.
Or skip the browser setup
ScreenshotNeo provides a GET endpoint that renders a URL and returns PNG, JPEG, WebP, or PDF. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response reports the result with X-Page-Verdict and X-Billed headers.
For a one-call capture, see the ScreenshotNeo documentation:
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)
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}`);
ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Its 63 options include full-page lazy-image loading, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper and page controls, custom CSS or JavaScript, pre-capture clicks, selector/delay/network-idle waits, request and resource blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous webhooks, bulk capture of 100 URLs per call, usage reporting, an OpenAPI specification, and familiar parameter names for easier migration.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteThe Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is included on every plan. Sign up for the free plan.
Frequently Asked Questions
Can Puppeteer execute an external script loaded by a page?
Yes. Puppeteer runs the page in a browser context, so scripts loaded by that document can execute when JavaScript is enabled. You still need to verify that the script loaded and that its output reached the page state you require.
Is a longer timeout enough to make rendering reliable?
No. A timeout only sets the maximum wait. Reliability comes from asserting a meaningful selector or function condition and collecting console and request failures when it is not met.
Should I read data with page.content() or page.evaluate()?
Use page.evaluate() when you need a specific DOM value or application state. It runs in the page context and lets you return a focused, serializable result instead of parsing the entire document.
Quick Recap
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.

