How to Wait for Dynamic Content to Load Before Taking a Screenshot in Puppeteer
Wait for the page state your screenshot needs: use a visible selector, a custom DOM condition, or network idle, then capture with Puppeteer.
To capture dynamic content with Puppeteer, wait for an observable condition that means the content you need is ready, then call page.screenshot(). Use page.waitForSelector() when a reliable element appears, page.waitForFunction() for an application-specific DOM condition, or page.waitForNetworkIdle() only when quiet network traffic is a suitable readiness signal. Navigation completing does not guarantee that client-side rendering or later data requests have finished.
Choose a readiness signal
The right wait depends on what “ready” means for the target page. Prefer a condition tied to the content in the image over a fixed delay: it is clearer, adapts to faster runs, and reports a timeout when the expected state never arrives.
| Wait method | Use it when | What it checks | Watch out for |
|---|---|---|---|
waitForSelector(selector, { visible: true }) |
A known element appears when the desired content is ready. | The selector exists and is visible. | An element that appears before its content is populated is not a sufficient marker. The documented default timeout is 30 seconds. |
waitForFunction() |
Readiness means a custom DOM state, such as enough cards and no loading message. | Your page-context function returns a truthy value. | Write a condition that reflects the actual state needed in the screenshot. |
waitForNetworkIdle() |
Network quiet is a useful signal for this page. | The page reaches network idle for the configured idle interval. | Network quiet does not prove the UI is correct or complete. Pages with ongoing requests may never become idle. |
Navigation waitUntil: 'networkidle2' |
You want to wait for a navigation lifecycle condition. | The navigation wait condition documented by Puppeteer. | It is not a universal replacement for an app-specific ready marker. |
For a stable, known marker, start with waitForSelector. Use a function when several facts must be true. Choose network idle only when the site’s network behavior makes that condition meaningful. Puppeteer’s screenshot guide shows Page.screenshot() for capture and includes networkidle2 in a navigation example; the selector and function waits let you express page-specific readiness directly.
Runnable example: wait for a visible element
This CommonJS script opens a page, waits for a visible readiness marker, and saves a full-page screenshot. Install Puppeteer with npm install puppeteer, save the code as screenshot.js, and run node screenshot.js https://example.com. Replace .results-ready with a selector that only becomes visible when the content you need is ready.
const puppeteer = require('puppeteer');
async function main() {
const url = process.argv[2];
if (!url) throw new Error('Usage: node screenshot.js <url>');
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.results-ready', {
visible: true,
timeout: 15_000,
});
await page.screenshot({ path: 'results.png', fullPage: true });
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
domcontentloaded gives the script an initial document before it waits for the application marker. You can use another navigation condition if it better fits the page, but keep the explicit readiness wait when the content is rendered later by JavaScript.
Wait for a custom DOM condition
Sometimes there is no single reliable element. A function can check multiple signals, such as the disappearance of a loading indicator and a minimum number of result cards:
await page.waitForFunction(() => {
const loading = document.querySelector('.loading');
const cards = document.querySelectorAll('.result-card');
return !loading && cards.length >= 3;
}, { timeout: 15_000 });
await page.screenshot({ path: 'results.png' });
The callback runs in the page context. The selectors and expected count above are examples: use the target page’s real loading state and the amount of content the capture requires. If the app exposes a more dependable ready marker, prefer that over guessing from incidental DOM details.
Wait for network idle
When the page’s meaningful content arrives through finite requests and then traffic settles, network idle may be useful:
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForNetworkIdle({ idleTime: 500 });
await page.screenshot({ path: 'page.png' });
Puppeteer documents a default idleTime of 500 ms and a default concurrency threshold of 0 for waitForNetworkIdle(). Network idle is a traffic condition, not a statement about your application’s correctness. Analytics, polling, streaming, or other ongoing requests can make it a poor fit. If a known element or DOM condition better represents readiness, wait for that instead.
Puppeteer’s screenshot guide also demonstrates navigation with waitUntil: 'networkidle2':
await page.goto(url, { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'page.png' });
This is a valid navigation wait choice when appropriate to the page. It still does not make an app-specific readiness condition unnecessary in every case.
Configure timeouts and capture options
Set timeouts according to the page and the maximum time your job can spend on a capture. Selector waits have a documented default timeout of 30,000 ms; pass timeout to change it. A timeout should fail the capture with a useful error instead of silently producing an image of a loading state.
await page.waitForSelector('.results-ready', {
visible: true,
timeout: 20_000,
});
await page.screenshot({
path: 'results.png',
fullPage: true,
type: 'png',
});
fullPage: true captures the full page rather than only the viewport. Choose the output path and screenshot options to match your downstream use. The key ordering stays the same: navigate, wait for the chosen readiness signal, then capture.
Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot contains a spinner or skeleton | The wait condition only confirms navigation or an element that appears before data is rendered. | Wait for a marker that represents populated content, or use waitForFunction() to check the required state. |
waitForSelector times out |
The selector is wrong, the element never becomes visible, or the page did not reach the expected state before the timeout. | Inspect the page’s actual DOM and loading behavior, correct the selector, and set a timeout appropriate to the page. Do not hide an incorrect condition with a sleep. |
waitForFunction never resolves |
The predicate does not match the page’s real state, or one of its assumptions (such as a minimum card count) is wrong. | Check each part of the condition against the rendered page and keep the predicate limited to what the screenshot needs. |
| Network idle wait stalls | The page continues making requests, for example due to polling or streaming. | Use a page-specific selector or DOM condition instead of waiting for all relevant traffic to stop. |
| Network idle resolves, but content is missing | The page became quiet before its UI reached the desired state. | Use an app-level readiness marker. Network silence alone does not establish that data was rendered correctly. |
| Capture is clipped | The screenshot uses the viewport dimensions while the desired content extends below it. | Use fullPage: true when a full-page image is required. |
Performance and reliability
- Use the narrowest meaningful condition. A specific selector or DOM predicate can finish as soon as the needed content is ready, while an unnecessarily broad wait can add latency.
- Avoid arbitrary fixed sleeps as the primary readiness mechanism. A sleep can be too short on a slow run and waste time on a fast one; it also gives no evidence that the page reached the right state.
- Choose timeouts that balance the page’s normal load behavior with the job’s overall deadline. Surface timeout errors so callers can distinguish a failed readiness wait from a successful image.
- For repeatable captures, make the readiness predicate explicit and keep it aligned with the content the screenshot is meant to show. A successful screenshot call does not itself certify that the application rendered the intended data.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return an image or PDF, so you do not need to launch and manage Puppeteer for a straightforward capture. See the API documentation for parameters and response details.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', image));
- Cookie banners are accepted and removed before capture, along with supported consent banners, newsletter popups, and chat widgets.
- Bot checks, blank pages, and failed loads are never billed; response headers identify the page verdict and billing status.
- An MCP server lets AI agents use screenshot tools, including
take_screenshot,get_page_info, andcapture_pdf. - 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does page.screenshot() wait for dynamic content?
No. It captures the page state when called. Add a wait for the readiness condition your page requires before calling it.
Should I always use networkidle2?
No. It is a documented navigation wait option, but ongoing requests and app-specific rendering can make it unsuitable as the only readiness signal.
What should the selector be?
Use an element that appears or becomes visible only when the content you want is ready. The correct selector depends on the target site.
Can I use a locator wait?
Locators are useful for finding and interacting with elements, but a locator wait is not a universal definition of when all screenshot content is ready. Tie the wait to the page state the image requires.


