October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
JavaScript

How to Capture Screenshots from Many URLs Efficiently with Puppeteer

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

Use one Puppeteer browser, process URLs through a bounded set of pages, wait for each site’s real readiness signal, and save every result under a deterministic filename. A sequential loop is simplest for small lists. For larger batches, a worker pool limits CPU and memory use while allowing one failed URL to be recorded without losing the rest.

What the workflow does

Puppeteer’s Page.screenshot() method captures a page and, when given path, writes the image to disk. One Browser can contain multiple Page instances, so a batch can reuse one browser process rather than launching a browser for every URL. This is an implementation pattern supported by Puppeteer’s browser model, not a published throughput benchmark.

The reliable sequence is:

  1. Read and validate the URL list.
  2. Launch one browser.
  3. Create a page for each job.
  4. Set a deliberate viewport and capture settings.
  5. Navigate with an appropriate wait condition.
  6. Wait for a site-specific selector when necessary.
  7. Capture the page and record success or failure.
  8. Close each page in finally, then close the browser.

Install Puppeteer and prepare inputs

Use a current Node.js project and install Puppeteer:

npm init -y
npm install puppeteer

Puppeteer is guaranteed to work with its bundled browser. If you use executablePath for a system browser, compatibility is not guaranteed. Browser launch has a documented default startup timeout of 30 seconds; set a longer timeout only when your environment needs it.

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

Create urls.txt with one absolute HTTP(S) URL per line:

https://example.com
https://stripe.com
https://www.mozilla.org/

Simple sequential capture

Sequential processing uses one page at a time. It has the smallest memory footprint and makes rate limiting easy to understand.

const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');

const urls = (await fs.readFile('urls.txt', 'utf8'))
  .split(/r?n/)
  .map(s => s.trim())
  .filter(Boolean);

await fs.mkdir('screenshots', { recursive: true });
const browser = await puppeteer.launch({ headless: true });

function fileName(url, index) {
  const host = new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '_');
  return path.join('screenshots', `${String(index).padStart(4, '0')}-${host}.png`);
}

const results = [];
try {
  for (let i = 0; i < urls.length; i++) {
    const url = urls[i];
    const page = await browser.newPage();
    try {
      await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
      const response = await page.goto(url, {
        waitUntil: 'networkidle2',
        timeout: 60000
      });
      if (response && response.status() >= 400) {
        throw new Error(`HTTP ${response.status()}`);
      }
      await page.screenshot({
        path: fileName(url, i),
        fullPage: true,
        type: 'png'
      });
      results.push({ url, ok: true });
    } catch (error) {
      results.push({ url, ok: false, error: error.message });
    } finally {
      await page.close();
    }
  }
} finally {
  await browser.close();
}

await fs.writeFile('results.json', JSON.stringify(results, null, 2));
console.table(results);

networkidle2 is the condition used in Puppeteer’s official screenshot example: it waits until there are no more than two active network connections. It is not a universal guarantee that an application has finished rendering. A continuously connected dashboard, stream, or analytics-heavy page may never reach the state you expect.

Use readiness checks that match the site

Wait for a known element

If the page has a stable completion marker, wait for it after navigation:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('[data-page-ready="true"]', { timeout: 30000 });
await page.screenshot({ path: output, fullPage: true });

Use a selector that belongs to the application, such as a report container or product heading. Avoid selectors that appear briefly during loading.

Wait for a controlled delay only when necessary

await page.waitForTimeout(2000);

A fixed delay is predictable but not adaptive: it can waste time on fast pages and still be too short on slow ones. Prefer a selector or application signal when available.

Handle lazy-loaded content

For long pages, full-page capture may trigger layout and lazy-loading behavior differently across sites. Scroll in increments before the final capture when images are loaded only near the viewport:

await page.evaluate(async () => {
  await new Promise(resolve => {
    let y = 0;
    const step = 700;
    const timer = setInterval(() => {
      window.scrollBy(0, step);
      y += step;
      if (y >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0);
        resolve();
      }
    }, 100);
  });
});

Bounded parallel workers for larger batches

Parallel pages can improve batch completion time on your own workload, but every page consumes browser and system resources and increases pressure on target sites. Puppeteer’s official references do not publish an optimal concurrency number. Start with a small configurable limit, measure CPU, memory, failures, and page latency, then adjust.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');

const urls = (await fs.readFile('urls.txt', 'utf8'))
  .split(/r?n/).map(x => x.trim()).filter(Boolean);
const concurrency = Number(process.env.CONCURRENCY || 4);
await fs.mkdir('screenshots', { recursive: true });

const browser = await puppeteer.launch({ headless: true });
const results = new Array(urls.length);
let next = 0;

async function worker() {
  while (true) {
    const index = next++;
    if (index >= urls.length) return;
    const url = urls[index];
    const page = await browser.newPage();
    const output = path.join('screenshots', `${String(index).padStart(4, '0')}.webp`);
    try {
      await page.setViewport({ width: 1365, height: 768, deviceScaleFactor: 1 });
      const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
      if (response && response.status() >= 400) throw new Error(`HTTP ${response.status()}`);
      await page.screenshot({ path: output, type: 'webp', quality: 82, fullPage: true });
      results[index] = { url, ok: true, output };
    } catch (error) {
      results[index] = { url, ok: false, error: error.message };
    } finally {
      await page.close();
    }
  }
}

try {
  await Promise.all(Array.from({ length: Math.min(concurrency, urls.length) }, worker));
} finally {
  await browser.close();
}
await fs.writeFile('results.json', JSON.stringify(results, null, 2));

This pool gives each URL an independent result. The indexed output preserves input order even though workers finish at different times. Keep the finally blocks: a navigation timeout or detached page must not leak a page, and the browser must close after all workers settle.

Screenshot options that matter

Option Use Important qualification
fullPage: true Capture the entire document. Output dimensions and file size can become very large.
clip Capture a specified rectangle. Use coordinates that exist at the chosen viewport and page state.
type Select PNG, JPEG, or WebP. Puppeteer documents PNG as the default.
quality Control JPEG/WebP compression. Range is 0–100 and does not apply to PNG.
omitBackground: true Hide the default white background. Transparency depends on page content and output support.
path Write the image to disk. Relative paths resolve from the current working directory; omit it to receive image data instead.

For one component, locate it and call the element handle’s screenshot method. Puppeteer scrolls the element into view when needed; the call fails if the element has been detached.

const card = await page.waitForSelector('.invoice-card');
await card.screenshot({ path: 'invoice-card.png' });

Naming, determinism, and output safety

  • Use an index plus a sanitized hostname or hash so duplicate paths cannot overwrite each other.
  • Fix viewport dimensions, device scale factor, format, quality, and full-page policy for comparable images.
  • Validate URLs before launching navigation and reject unsupported schemes.
  • Write a JSON or CSV manifest containing URL, output path, timestamp, status, and error text.
  • Do not treat an HTTP error page as a successful business-page capture; inspect the navigation response when status matters.

Troubleshooting common failures

Navigation timeout

The site may be slow, blocked, or waiting indefinitely for network activity. Increase the timeout only when justified, switch from networkidle2 to domcontentloaded plus a selector, and record the failed URL. Do not retry unlimited times.

Blank or incomplete image

Capture after the actual content marker appears, verify the viewport, and test whether lazy-loaded content requires scrolling. A full-page image can also expose application behavior that differs from the visible viewport.

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

High memory or CPU use

Lower the worker limit, close pages promptly, avoid launching one browser per URL, and measure again on the real URL mix. There is no official universal concurrency recommendation.

Detached node error

The page replaced the element before its screenshot completed. Re-query the selector after the final render signal and capture immediately.

Authentication or consent blocks

Provide the required cookies, headers, or login flow only when you are authorized. Consent banners, anti-bot checks, rate limits, and terms vary by site; respect each target’s rules.

System-browser incompatibility

Prefer Puppeteer’s bundled browser. An alternative executable path is not covered by Puppeteer’s compatibility guarantee.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It removes cookie/consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, with X-Page-Verdict and X-Billed headers explaining the result. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options, including full-page and element capture, device presets, custom CSS/JavaScript, waits, blocking rules, headers, cookies, geolocation, caching, signed links, webhooks, bulk capture of up to 100 URLs per call, and usage reporting.

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

The free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Create a free ScreenshotNeo account.

FAQ

Should every URL get a new browser?

No. Reuse one browser and create independent pages, closing each page after its job.

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

Is networkidle2 always best?

No. Choose the readiness condition that matches the target application, often a known selector.

What concurrency should I use?

There is no documented universal number. Start small and benchmark your actual pages and machine while respecting target-site limits.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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.

Read next

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.