How to Take a Puppeteer Screenshot After a Page Finishes Loading
Wait for the right page-ready signal before calling Puppeteer’s screenshot API. Compare lifecycle events, network idle, and selector waits, with runnable examples and fixes for common failures.
To take a Puppeteer screenshot after a page finishes loading, navigate with a readiness condition, then call page.screenshot(). For a general-purpose wait, Puppeteer’s screenshot guide uses waitUntil: 'networkidle2':
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
});
await page.screenshot({ path: 'page.png' });
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
This waits for navigation and network activity to settle according to Puppeteer’s lifecycle rule. It does not prove that every application has finished rendering its most important content. Choose the condition that matches what the page must show in the screenshot. See the official Puppeteer screenshot guide and lifecycle event documentation.
1. Install Puppeteer and run the basic capture
In a new Node.js project, install Puppeteer:
npm install puppeteer
Save the first example as screenshot.js and run node screenshot.js. Puppeteer launches a browser, opens a page, waits for networkidle2, writes page.png in the current directory, and closes the browser even if navigation or capture fails.
The navigation option waitUntil can be one event or an array of events. When you specify multiple events, navigation succeeds after all of them have fired. Puppeteer’s documented lifecycle choices are:
| Value | What it waits for | Good fit |
|---|---|---|
load |
The window load event, which waits for dependent resources such as stylesheets and images. | Pages where the load event is a useful readiness signal. |
domcontentloaded |
The document’s DOMContentLoaded event. | Pages where you will wait for a specific element or app state afterward. |
networkidle0 |
A 500 ms window with no more than zero network connections. | Pages expected to become fully quiet before capture. |
networkidle2 |
A 500 ms window with no more than two network connections. | A general-purpose network-settled wait that tolerates a small number of active requests. |
The default navigation wait is load, and the default navigation timeout is 30,000 ms. These are different decisions: waitUntil defines the success condition, while timeout limits how long Puppeteer waits for it. See Puppeteer’s wait options.
2. Choose a readiness signal that matches the page
Network idle is useful when the page’s content appears as its requests finish. It can be a poor fit when useful content renders later through client-side work, or when analytics, polling, streaming, or another long-lived request keeps the network busy. In those cases, wait for the specific content your screenshot needs.
Wait for a page-specific element
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
});
// Replace this illustrative selector with an element that signals
// the content required in your screenshot.
await page.waitForSelector('[data-ready="true"]', {
visible: true,
timeout: 15000,
});
await page.screenshot({ path: 'page-ready.png' });
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The selector is an example, not a selector guaranteed to exist on a site. Pick a stable element that appears only when the needed content is ready. Waiting for a generic container that exists before its data loads may still produce an early screenshot.
Wait for network idle after navigation
You can also navigate at one lifecycle point and then call page.waitForNetworkIdle() explicitly:
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
});
await page.waitForNetworkIdle({
idleTime: 1000,
timeout: 15000,
});
await page.screenshot({ path: 'network-idle.png' });
idleTime defaults to 500 ms; this method always waits at least for the configured idle period. Use its options to tune the idle window and timeout for the target page. If the page maintains active connections, a selector or application-specific readiness condition is usually more reliable. See the method documentation and its options.
3. Capture a full page or one element
By default, page.screenshot() captures the current viewport and produces PNG output unless you select another supported type. Set fullPage: true to capture the full document:
await page.screenshot({
path: 'full-page.png',
fullPage: true,
});
To capture a particular element, wait for it and call the element handle’s screenshot() method. Puppeteer scrolls the element into view when needed:
const report = await page.waitForSelector('.report', {
visible: true,
});
if (!report) {
throw new Error('Report element was not found');
}
await report.screenshot({ path: 'report.png' });
An element screenshot fails if the element has been detached from the DOM. This can happen when a framework replaces the element after it appears. If that is possible, wait for the final stable state and look up the element again before capturing. For options and output details, see ScreenshotOptions and Page.screenshot().
4. Use the wait and screenshot options deliberately
waitUntil: Sets the navigation lifecycle condition. Use a selector or a separate network-idle wait when lifecycle events do not represent application readiness.timeout: Sets how long navigation or a wait may take before it rejects. Raise it only when the target legitimately needs longer; an unlimited wait can leave a job stuck.path: Writes the screenshot to a file. Without a path, the screenshot call returns image data instead.fullPage: Captures beyond the viewport when set totrue; it defaults tofalse.- Screenshot type: PNG is the default. Consult the current screenshot options for supported formats and format-specific settings.
- Element handle: Use an element’s screenshot method when the output should contain one component rather than the page.
Use the official ScreenshotOptions reference for the complete option list for your installed Puppeteer version. The documentation currently labels its API reference as Puppeteer 25.12.0; options may differ across versions.
5. Add practical reliability safeguards
- Set a finite timeout. Navigation, selector waits, and network-idle waits can otherwise take longer than the job should allow.
- Always close the browser. Put
browser.close()in afinallyblock so errors do not leave browser processes running. - Use a meaningful readiness condition. Wait for the content needed in the output, not merely for an event that happens to fire earlier.
- Handle expected page failures. A timeout, navigation error, missing selector, or detached element should be reported with the URL and capture step so the failure can be diagnosed.
- Keep screenshot dimensions in mind. Full-page images can be much larger than viewport captures and take more memory to encode and store.
For repeated captures, reuse a browser process where appropriate and create a fresh page per job, while ensuring each page is closed after use. Limit parallel captures to the capacity of the machine: each browser page consumes resources, and very large full-page images can increase memory pressure. Puppeteer’s screenshot guide documents the capture calls; resource limits depend on your workload and deployment environment, so measure them on representative pages.
6. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
Navigation timeout |
The selected lifecycle condition never completed before the timeout, perhaps because requests remain active. | Choose a condition better suited to the page, such as domcontentloaded followed by a selector wait; increase the timeout only when the page genuinely needs more time. |
| Screenshot is blank or missing content | The screenshot ran after navigation but before client-rendered content became ready. | Wait for an element or state that indicates the required content is visible, then capture. |
networkidle0 never resolves |
The page continues making requests or holds connections open. | Try networkidle2, use a bounded waitForNetworkIdle(), or wait for a page-specific selector instead. |
Waiting for selector ... failed |
The selector is wrong, the element is absent in this page state, or it appears later than the wait timeout. | Inspect the actual DOM and selector, check for redirects or alternate page states, and adjust the timeout if appropriate. |
| Element screenshot reports a detached node | The page replaced or removed the element after Puppeteer found it. | Wait for the final content state, query the element again, and capture the fresh handle. |
| Screenshot file is missing | The path is relative to a different working directory than expected, or the call failed before writing. | Use a known absolute path or confirm the process working directory; log and handle screenshot errors. |
| Capture works locally but fails in deployment | The browser may not be installed or launchable in that environment, or the target may behave differently there. | Check the deployment’s Puppeteer installation and browser launch requirements, then log the failing stage and target URL. |
7. cURL, Python, and Node.js alternative
If you do not need to manage a local browser, ScreenshotNeo provides a website screenshot API. One GET request can return an image or PDF. The examples below save a WebP response for https://stripe.com; the ScreenshotNeo documentation describes the API and its options.
cURL
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()
with open("shot.webp", "wb") as f:
f.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 request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Keep API keys on the server and out of public browser code. ScreenshotNeo accepts the parameter names used by other screenshot APIs to make switching easier. Its feature set includes full-page and element capture, waits, custom headers and cookies, caching, bulk capture, and PDF options; consult the docs for parameter names and supported values.
8. Or skip the browser setup
ScreenshotNeo handles the remote browser and returns the capture from one API call. Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
There are 1,000 screenshots per month on the free plan with no card required; paid plans start at $5 for 3,000 screenshots. Learn about ScreenshotNeo, read the API docs, or sign up for 1,000 free screenshots a month, no card required.
9. Frequently asked questions
Does networkidle2 guarantee the page is fully rendered?
No. It indicates that network connections stayed within the documented threshold for the idle window. Application work can still render content afterward, so wait for the content your capture needs when that matters.
Should I use networkidle0 or networkidle2?
Use networkidle0 when the page can become fully quiet; use networkidle2 when allowing up to two active connections is more practical. For pages that never settle, use an explicit readiness element.
Can I take a screenshot without saving it to a file?
Yes. Omit path and handle the returned screenshot data in your program. See the Page.screenshot() API for the return type and current options.
Why does a full-page screenshot take longer or use more memory?
It captures a larger image than the viewport screenshot, so rendering and encoding more pixels can require more time and memory. Use viewport or element captures when they meet the output requirement.


