How to Screenshot Pages After Network Idle in a Batch with Playwright
Capture a batch of pages after Playwright network idle, with bounded concurrency, reliable readiness checks, runnable code, and fixes for common failures.
To screenshot pages after network idle in a batch, navigate each page with page.goto(url, { waitUntil: 'networkidle' }), then call page.screenshot(). Playwright defines networkidle as no network connections for at least 500 ms, but discourages using it as a test-readiness signal. If a particular element or application state matters, wait for that condition explicitly.
The example below uses Node.js and Playwright, writes one full-page PNG per URL, limits parallel work, and closes each page even if capture fails. Install Playwright and its Chromium browser first:
npm init -y
npm install playwright
npx playwright install chromium
1. Capture a batch serially
A serial loop is easiest to understand and keeps browser resource use low. Save this as batch-screenshots.js and run node batch-screenshots.js:
const { chromium } = require('playwright');
const fs = require('node:fs/promises');
const path = require('node:path');
const urls = [
'https://example.com/',
'https://playwright.dev/',
];
(async () => {
const outputDir = path.resolve('screenshots');
await fs.mkdir(outputDir, { recursive: true });
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
});
try {
for (const [index, url] of urls.entries()) {
const page = await context.newPage();
try {
const response = await page.goto(url, {
waitUntil: 'networkidle',
timeout: 45_000,
});
if (response && !response.ok()) {
console.warn(`${url}: HTTP ${response.status()}`);
}
const file = path.join(outputDir, `page-${String(index + 1).padStart(3, '0')}.png`);
await page.screenshot({ path: file, fullPage: true });
console.log(`Saved ${file}`);
} catch (error) {
console.error(`Failed ${url}: ${error.message}`);
} finally {
await page.close();
}
}
} finally {
await context.close();
await browser.close();
}
})();
The HTTP status check is useful because navigation can complete for an error response such as 404; whether to keep or reject that screenshot depends on the batch’s purpose. The index-based filenames are deterministic for a fixed URL ordering and avoid collisions between URLs with the same basename.
2. Choose the readiness signal
Network idle after navigation
page.goto(url, { waitUntil: 'networkidle' }) makes navigation wait for the network-idle lifecycle condition. The Page API also offers load, domcontentloaded, and commit. Network idle may be a practical settling heuristic for pages whose requests finish, but quiet networking does not prove that a specific image, lazy-loaded section, animation, or application state is ready.
Wait for a meaningful element
For predictable capture, navigate at a lifecycle point and then wait for the content the screenshot actually needs. This example waits for a page-specific selector:
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
await page.locator('[data-page-ready="true"]').waitFor({
state: 'visible',
timeout: 15_000,
});
await page.screenshot({ path: outputPath, fullPage: true });
Replace the selector with an element that represents the required state on your target pages. If the page has no stable readiness marker, use a locator for its main content or another observable condition. Avoid relying on a fixed sleep as the primary readiness strategy: it can waste time on fast pages and still be too short on slow ones. Playwright discourages waitForTimeout() for production tests.
Wait for network idle after navigation
If navigation already committed and you want to wait for the lifecycle state afterward, use:
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle', { timeout: 30_000 });
await page.screenshot({ path: outputPath, fullPage: true });
If the requested state has already happened in the current document, waitForLoadState() resolves immediately. Persistent polling or streaming connections can prevent network idle from occurring, so use a content-specific wait where those requests are expected.
3. Run a batch with bounded concurrency
For a larger list, bounded concurrency can reduce total wall-clock time while preventing an unbounded number of pages from competing for memory and CPU. There is no universally correct concurrency number; start small and tune it for page weight and the machine running Chromium. This worker-pool example continues after individual URL failures and reports a summary:
const { chromium } = require('playwright');
const fs = require('node:fs/promises');
const path = require('node:path');
const urls = [
'https://example.com/',
'https://playwright.dev/',
'https://www.wikipedia.org/',
];
const concurrency = 3;
(async () => {
const outputDir = path.resolve('screenshots');
await fs.mkdir(outputDir, { recursive: true });
const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
let nextIndex = 0;
const failures = [];
async function worker() {
while (true) {
const index = nextIndex++;
if (index >= urls.length) return;
const url = urls[index];
const page = await context.newPage();
try {
const response = await page.goto(url, { waitUntil: 'networkidle', timeout: 45_000 });
if (response && !response.ok()) {
throw new Error(`HTTP ${response.status()}`);
}
const file = path.join(outputDir, `page-${String(index + 1).padStart(3, '0')}.png`);
await page.screenshot({ path: file, fullPage: true });
console.log(`Saved ${file} (${url})`);
} catch (error) {
failures.push({ url, error: error.message });
console.error(`Failed ${url}: ${error.message}`);
} finally {
await page.close();
}
}
}
try {
await Promise.all(Array.from({ length: Math.min(concurrency, urls.length) }, worker));
} finally {
await context.close();
await browser.close();
}
console.log(`Completed ${urls.length - failures.length}/${urls.length}; failed ${failures.length}`);
if (failures.length) process.exitCode = 1;
})();
Each worker takes the next index synchronously before awaiting browser work, so no two workers claim the same URL. If a page fails, its error is recorded and the worker moves on. For very large batches, persist results as they complete rather than retaining extensive diagnostics only in memory.
4. Organize pages, contexts, and output
- Reuse a context for related pages: pages in one
BrowserContextshare context-level settings and session state. Create pages withcontext.newPage(). This is useful when the batch should use consistent cookies, viewport, locale, or other context configuration. - Use separate contexts for isolation: create a context per independent session or configuration. This avoids sharing context state, at the cost of more setup and resources.
- Close pages promptly: a worker should close its page in
finally. Close the context and browser in an outerfinallyso errors do not leave browser processes running. - Choose safe filenames: batch indices are simple and collision-free for a fixed input order. If names must follow URLs, sanitize host and path components and still add a unique suffix; URLs can contain characters unsuitable for paths and different URLs can map to the same sanitized name.
- Keep capture conditions consistent: set viewport and use the same browser engine, browser version, operating system, locale, and rendering conditions when comparing screenshots. Playwright notes that rendering may vary with host OS, browser version, settings, hardware, power source, and headless mode.
5. Screenshot options that matter
The core capture call is page.screenshot(options). For a batch, choose options that make output predictable:
pathwrites the screenshot to a file. Use a unique path for every URL.fullPage: truecaptures the full scrollable page; omit it for the current viewport. Very long pages can produce large images or hit browser limits.typeselects an image format such as'png','jpeg', or'webp'where supported by the installed Playwright version. Use PNG for lossless visual comparisons; JPEG/WebP can reduce storage when lossy output is acceptable.qualityapplies to lossy formats such as JPEG and WebP; it does not make PNG smaller.animationscan disable or fast-forward animations for more stable captures. Check the installed version’s screenshot API for exact supported values.omitBackgroundcan preserve transparency for supported formats instead of painting the default page background.
For option details and version-specific behavior, consult the Playwright Page screenshot API. When the batch is for visual regression tests, Playwright Test’s expect(page).toHaveScreenshot() waits for two consecutive screenshots to match before comparing with the expectation. That assertion workflow is different from simply writing output files.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
page.goto: Timeout ... exceeded |
The page never reached the chosen lifecycle state, or navigation exceeded the timeout. | Check whether the site keeps requests open. Use domcontentloaded or commit and wait for a page-specific locator. Increase the timeout only when the site genuinely needs more time. |
| Screenshot is blank or missing expected content | Network idle occurred before client-side content or a required element was ready, or the site returned an error page. | Check the navigation response and URL, then wait for the expected locator or application state before capture. |
| Some batch files overwrite one another | Output names are derived from a basename or URL fragment that is not unique. | Use the input index or include a unique suffix in every filename. |
| Browser exits before all captures finish | The browser is closed while worker tasks are still running, or an error bypasses cleanup. | Await every worker before closing; put page, context, and browser cleanup in finally blocks. |
| Batch is slow or the process runs out of memory | Pages are opened without a concurrency limit, full-page images are large, or pages are not closed. | Use a bounded worker pool, close each page after capture, and reduce concurrency or capture the viewport when full-page output is unnecessary. |
| Visual snapshots differ across runs | Browser or host rendering conditions changed, or dynamic content and animation changed. | Keep the environment and viewport consistent; disable or wait out relevant animations and wait for stable, meaningful content. |
| Playwright cannot launch Chromium | The browser binary has not been installed for the current Playwright package or required host dependencies are unavailable. | Run npx playwright install chromium; on supported Linux environments, consult the Playwright installation guidance for system dependencies. |
7. Performance, reliability, and cost
In a local Playwright batch, total time depends on navigation, waiting, screenshot rendering, and writing files. Serial execution is predictable; bounded parallelism can improve throughput until CPU, memory, network, or target-site limits become the bottleneck. Track per-URL duration and failures so a slow page does not hide the rest of the batch’s status.
Network idle adds at least the documented 500 ms quiet period after the network becomes idle, and may wait much longer or time out if requests continue. For a specific target state, a selector wait can finish sooner and better express what the screenshot needs. Avoid aggressive concurrency against sites you do not control; handle failures per URL and consider retrying only transient failures with a finite retry limit.
Playwright itself is an open-source browser automation library, but running a batch still consumes your compute, network bandwidth, storage, and maintenance time. Large full-page images increase storage and transfer costs. A hosted screenshot API trades browser setup and runtime management for per-plan usage; compare its billing rules and output options against your workload.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF; see the API documentation. This example saves a screenshot response:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
9. FAQ
Does network idle mean every image has loaded?
No. It means the documented network-quiet interval occurred. Wait for the specific image or page element your capture requires.
Can I use one browser context for many URLs?
Yes. A context can contain multiple pages. Reuse it when pages should share its session and settings; use separate contexts when they need isolation.
Should visual regression use saved screenshots or screenshot assertions?
Use saved files for an ordinary capture archive. For Playwright Test comparisons against baselines, use toHaveScreenshot() and keep the rendering environment consistent.
What should I do when a site never becomes idle?
Navigate using an earlier lifecycle state, then wait for the content condition that matters. Persistent requests can make a global network-quiet condition unsuitable.


