Menu
Sign up

Speed up Puppeteer screenshots: what we measured, and what did not help

Slow Puppeteer screenshots, measured: browser reuse, waitUntil, blocking slow scripts, PNG vs JPEG vs WebP, optimizeForSpeed and parallel pages.

Published November 6, 2026 · 5 min read

A slow Puppeteer screenshot is almost never slow because of the screenshot. In our measurements the capture itself took 50 to 300 milliseconds; loading the page took seconds. So the changes that help are about what you wait for, what you load, and how many browsers you start. Below are seven changes, each measured on 2026-11-06 with Puppeteer 25.7.0 in the official ghcr.io/puppeteer/puppeteer image, limited to 2 CPU cores, against a local test page: a heading, 30 images that each take 100 ms to arrive, and one third-party-style script that takes 1.5 s. Absolute times on your pages will differ; the differences between the options are what carry over.

1. Keep one browser running

Setup Time per screenshot
puppeteer.launch() and close() for every shot 2,790 ms
One browser, a new page per shot 2,152 ms
One browser, one page reused 2,103 ms

Starting Chrome cost about 640 ms per shot here — more on a busy server or a cold container. Launch once, keep the browser, and open a page per job. Reusing the same page saved almost nothing more, and a fresh page avoids state from the previous site (cookies, service workers, a dialog left open), so a new page per job, closed afterwards, is the better default.

const browser = await puppeteer.launch();           // once, at startup

async function screenshot(url) {
  const page = await browser.newPage();
  try {
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
    return await page.screenshot({ type: 'jpeg', quality: 80 });
  } finally {
    await page.close();                              // always, or pages pile up
  }
}

Watch for browser.on('disconnected') and relaunch when it fires, and consider restarting the browser now and then on long-running workers, since a long-lived Chrome process can grow in memory.

2. Choose waitUntil on purpose

waitUntil Time Images loaded Page with a request that never ends
domcontentloaded 58 ms 0 of 30 19 ms
load 2,038 ms 30 of 30 2,050 ms
networkidle2 1,116 ms 30 of 30 1,220 ms
networkidle0 2,570 ms 30 of 30 timed out after 15 s

What this shows:

  • domcontentloaded is fast and empty — none of the images had arrived. Use it only if you then wait for something specific (page.waitForSelector, document.fonts.ready, image complete).
  • load waits for the slow script. The async script that took 1.5 s held the load event, although the page looked finished long before.
  • networkidle2 (no more than two connections for 500 ms) was the fastest option that had every image, and it shrugged off the slow script.
  • networkidle0 never finished on a page with one hanging request (a long poll, analytics beacon or chat widget does this).

Puppeteer's documentation for page.goto defines the four values. A good default is networkidle2 with an explicit timeout, plus a short wait for the element you care about.

3. Do not load what the screenshot does not need

Aborting the slow script with request interception brought the networkidle0 load from 2,570 ms down to 1,238 ms on the test page. In practice, block by domain:

await page.setRequestInterception(true);
page.on('request', (request) => {
  const url = request.url();
  if (/googletagmanager|google-analytics|hotjar|intercom|doubleclick/.test(url)) return request.abort();
  request.continue();
});

Block by domain rather than by type: blocking every script breaks pages that render with JavaScript. Analytics, chat widgets, ad and A/B-testing scripts are safe to drop for a screenshot and are often the slowest requests on a page.

4. Pick the format by speed and size

Capture time for a 1280×800 viewport, average of five, and file size (the size depends heavily on the page — ours had many small photo-like images):

Format 1× 2× (retina)
PNG (default) 77 ms, 255 KB 290 ms, 956 KB
PNG, optimizeForSpeed: true 46 ms, 302 KB 117 ms, 1,249 KB
JPEG, quality 80 51 ms, 551 KB 117 ms, 1,424 KB
WebP, quality 80 164 ms, 552 KB 484 ms, 1,415 KB

optimizeForSpeed (Puppeteer docs) uses faster PNG compression: 2.5 times faster at 2× here, for a file about 30% larger. WebP was the slowest to encode. If you capture thousands of pages, PNG with optimizeForSpeed or JPEG is the cheap choice; compress afterwards if storage matters more than time. One more reason to be careful with WebP and JPEG: for very tall full-page captures they return an empty buffer (WebP above 16,383 px, JPEG above 65,500 px) — see full-page screenshots.

Retina (deviceScaleFactor: 2) is four times the pixels, and every format took 2.3–3.8 times longer. Use it when the image will be shown at retina size, not by default.

5. Run pages in parallel — across different sites

Eight screenshots with one browser:

One at a time Eight at once
Eight different hosts 5,166 ms 2,038 ms
The same host eight times 5,290 ms 4,684 ms

Parallel pages pay off across sites. Against one site they barely helped: Chrome allows a limited number of connections per host, and all pages of one browser share them, so eight pages of the same site queue behind each other. For many pages of one site, the site's server sets the pace; spreading the work over time (or over separate browser processes, each with its own connections — measure it for your site) is the way to go faster, not more tabs.

Cap the concurrency (a small pool, for example with a counter or p-limit): every open page holds memory and CPU, so find the number where your machine stops getting faster.

6. Make the viewport no bigger than needed

A screenshot's cost grows with its pixels: the 2× capture above has four times the pixels of 1× and took up to 3.8 times as long. Set the viewport to the size you will publish, and capture an element (elementHandle.screenshot()) instead of the full page when you need only part of it.

7. Cache, and do not screenshot twice

The fastest screenshot is the one you do not take. If the same URL is requested again within minutes, serve the stored file. Key the cache on everything that changes the image: URL, viewport, scale, format, and the time window after which the page may have changed.

What did not make a measurable difference

  • Reusing the same page instead of a new page per shot: 2,103 vs 2,152 ms.
  • captureBeyondViewport for viewport captures: it matters for full-page captures, not here.

Or skip the browser

If what you are building is a screenshot feature rather than a browser farm, Shareglint's screenshot API runs the pool, the waiting rules, cookie-banner handling and the cache for you: the same URL with the same options within 24 hours is returned without rendering, and a viewport capture costs 1 render of quota (JavaScript example, pricing). For pages behind a login or on your own network, your own Puppeteer — with the changes above — is the right tool.

FAQ

Why is my Puppeteer screenshot so slow?

Usually not the screenshot: in our tests the capture took 50–300 ms and loading the page took seconds. The waitUntil you choose, slow third-party scripts and launching a new browser per shot cost far more than the image encoding.

Which waitUntil is fastest?

domcontentloaded returns first but before images load. On our test page networkidle2 was faster than load and still had every image, while networkidle0 never finished on a page with one long-polling request. Use networkidle2, or load plus an explicit wait for what you need.

Is JPEG faster than PNG in Puppeteer?

Yes, and so is PNG with optimizeForSpeed: on a 1280×800 viewport at 2× they took about 117 ms against 290 ms for a default PNG. WebP was the slowest, about 484 ms.

Does running pages in parallel help?

Across different sites, yes: eight sites at once took 2.0 s against 5.2 s one after another. Eight pages of the same site at once gained little, because Chrome limits connections per host and the pages share them.