How to Improve Puppeteer Performance
Improve Puppeteer performance by measuring the real bottleneck, choosing the right headless mode, and tuning capture work without sacrificing correctness.

Puppeteer performance depends on the work your automation actually does: launching Chrome, loading a page, waiting for it, rendering output, or processing results. Start by measuring those phases on representative pages. One documented configuration worth comparing is headless: 'shell': Puppeteer says Chrome Headless Shell can be more performant for automation that does not need the complete Chrome feature set, though its behavior does not completely match regular Chrome. There is no universal speedup or setting that makes every workload faster.
This guide shows how to measure a Puppeteer job, compare headless modes, reduce unnecessary work, and preserve reliable screenshots and PDFs. For output correctness, keep the required viewport, fonts, assets, page area and interactions in the benchmark.
1. Measure before changing settings
Record elapsed time separately for browser startup, navigation, application readiness, capture, and shutdown. Also record success rate, timeouts, output size, and a correctness check such as image dimensions or a visual comparison. Run multiple representative URLs, including slow and content-heavy pages, and compare medians and tail latency. A single warm run can hide cold-start and network variation.

Keep the environment stable: same Puppeteer version, browser binary, machine or container limits, URL set, concurrency, viewport, and output settings. Compare one change at a time. For repeated jobs, report cold and warm behavior separately because reusing a browser removes startup from later tasks.
Runnable baseline and headless-mode comparison
Install Node.js and Puppeteer with npm install puppeteer. The package downloads a compatible browser. Save this as bench.mjs, then run node bench.mjs. It records phase timings for both documented headless choices. Use URLs you are authorized to automate.
import puppeteer from 'puppeteer';
import { performance } from 'node:perf_hooks';
const urls = [
'https://example.com',
'https://www.wikipedia.org/',
];
for (const headless of [true, 'shell']) {
const start = performance.now();
const browser = await puppeteer.launch({ headless });
const launched = performance.now();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900, deviceScaleFactor: 1 });
for (const url of urls) {
const navStart = performance.now();
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 45000,
});
const navigated = performance.now();
await page.screenshot({ path: `shot-${headless === true ? 'new' : 'shell'}.png` });
const captured = performance.now();
console.log(JSON.stringify({
headless, url, status: response?.status(),
navigationMs: Math.round(navigated - navStart),
screenshotMs: Math.round(captured - navigated),
}));
}
} finally {
await browser.close();
console.log(JSON.stringify({
headless,
startupMs: Math.round(launched - start),
totalMs: Math.round(performance.now() - start),
}));
}
}
This is a starting point, not a benchmark result. The example uses domcontentloaded to define a consistent navigation boundary; it does not guarantee that a client-rendered application or late images are ready. Replace it with the readiness condition your output requires and apply that same condition to both modes. Puppeteer describes headless: true as its newer headless mode and headless: 'shell' as Chrome Headless Shell. See the official headless modes guide.
2. Choose the headless mode that meets your requirements
A normal puppeteer.launch() is equivalent to { headless: true }. Puppeteer documents Shell as currently more performant for automation that does not need the complete Chrome feature set. That guidance is qualitative; it does not promise a gain for your site, deployment, or output.

| Mode | When to evaluate it | Trade-off |
|---|---|---|
true |
Default starting point, especially when regular Chrome behavior matters. | May include capabilities your automation does not need. |
'shell' |
Automation tasks that do not require the complete Chrome feature set. | Behavior does not completely match regular Chrome; validate compatibility and output. |
false |
Debugging with a visible browser. | Headful mode changes the environment and is generally not a production performance shortcut. |
Test the pages and interactions your automation uses: authentication, downloads, media, dialogs, custom browser behavior, and screenshot or PDF output. Compare both correctness and elapsed time. If a page behaves differently in Shell, retain regular headless Chrome for that job.
3. Remove avoidable work from each job
Reuse a browser when jobs are related
Launching a browser has a cost. If you process several pages in one worker, launch once and create a fresh page for each job, then close each page. Close the browser when the worker stops. This avoids repeated startup while limiting page state leakage. Do not share one page between unrelated jobs or allow cookies, local storage, or navigations to carry over when isolation matters.
const browser = await puppeteer.launch({ headless: 'shell' });
try {
for (const url of urls) {
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.screenshot({ path: `capture-${Date.now()}.png` });
} finally {
await page.close();
}
}
} finally {
await browser.close();
}
For parallel work, cap concurrent pages and browsers based on measured memory and CPU use. More concurrency can increase queueing, page load time, and failures if the host is saturated. Monitor the whole worker, not just an individual fast run.
Wait for the condition you need
Navigation wait modes represent different events. domcontentloaded waits for initial document parsing; load includes load-event resources; network-idle waits can be unsuitable for sites with analytics, polling, or persistent connections. A page can also finish navigation before a single-page app has rendered its useful content. For such pages, wait for an application-specific selector or readiness signal, with a timeout.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForSelector('[data-report-ready="true"]', { timeout: 15000 });
Do not replace a wait with a shorter delay merely to improve a timing number. That can produce blank, partial, or inconsistent output. Likewise, network-idle can be slower or never occur on pages with ongoing requests. Pick the least expensive condition that still proves the needed content is ready.
4. Tune screenshot work without changing the result
Capture only the area and format you need. Puppeteer’s screenshot options include fullPage, clip, type, quality, encoding, and optimizeForSpeed. The API reference does not quantify their speed or quality trade-offs, so measure them with your image requirements.
await page.screenshot({
path: 'result.webp',
type: 'webp',
quality: 80,
fullPage: false,
optimizeForSpeed: true,
});
Quality applies to JPEG and WebP, not PNG. Choose the output format based on downstream requirements; changing format may change both file size and appearance. A viewport capture does less page-area work than a full-page capture, but only use it if the omitted content is not required. A clip can constrain the captured region. Avoid asserting an unmeasured throughput gain from optimizeForSpeed; compare latency, file size, and visual quality in your workload. See Puppeteer’s ScreenshotOptions API.
Large and lazy-loaded pages
Full-page capture can involve a tall document, large images, and content loaded as the page scrolls. If the task requires the entire page, verify that lazy images are loaded and that the final screenshot dimensions are correct. If only a component is needed, capture its bounding region instead of the whole document. Do not remove page content, block required resources, or change device scale just to shorten time if fidelity is part of the job.
5. Tune PDF generation carefully
Puppeteer’s PDF guide shows navigation with waitUntil: 'networkidle2' before page.pdf(), and PDF generation waits for fonts by default. Its PDF options also include page size, margins, landscape, page ranges, print backgrounds, scale, and a default 30-second timeout. These choices affect what output is produced and when it is ready.
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
waitForFonts: true,
timeout: 30000,
});
Only disable font waiting when you have established that fonts are already available and the output remains correct. Otherwise, the PDF may use fallback fonts or have different line breaks. Reduce the printed page range only when the requested document permits it. The PDF guide and PDFOptions API document these behaviors and settings.
6. Diagnose whether the delay is Node, browser, or network
Use phase timings and logs to locate the slow segment before changing launch arguments. Puppeteer’s debugging guide distinguishes Node-side code from browser-side code and browser internals. Attach page console and error handlers; enable dumpio: true temporarily to forward browser process output.
const browser = await puppeteer.launch({ headless: true, dumpio: true });
const page = await browser.newPage();
page.on('console', message => console.log('PAGE:', message.type(), message.text()));
page.on('pageerror', error => console.error('PAGE ERROR:', error.message));
page.on('requestfailed', request => console.error(
'REQUEST FAILED:', request.url(), request.failure()?.errorText
));
Verbose process logs can be noisy, so enable them while diagnosing and turn them off when no longer needed. The official debugging guide describes these diagnostics. They help identify causes; they are not themselves optimizations.
7. Startup, browser versions, and timeouts
Puppeteer’s LaunchOptions reference documents a 30,000 ms default startup timeout. Increasing it can prevent slow but valid startup from failing; it does not make startup faster. Setting it to zero disables the timeout, which can leave a stuck worker waiting indefinitely unless you implement another deadline.
The default browser is Chrome. Puppeteer says it is only guaranteed to work with its bundled browser; an executablePath or system channel can create version and behavior differences. If you use a custom binary, pin and validate it with the Puppeteer release you deploy. Changing the binary may help a packaging constraint, but the docs do not establish a performance improvement from doing so. Consult LaunchOptions.
8. Common performance problems and fixes
| Symptom | Likely cause | Practical fix |
|---|---|---|
| Every job has a large fixed delay | Browser launched for each URL. | Measure launch separately; reuse one browser for a bounded group of jobs. |
| Navigation hangs or times out | Wait condition never occurs, persistent traffic, slow server, or overloaded worker. | Log failed requests; use an app readiness selector or suitable navigation event; retain a finite timeout. |
| Fast screenshots are blank or incomplete | Capture happens before client rendering, fonts, or lazy images are ready. | Wait for a meaningful selector or state and verify output before optimizing. |
| Shell output differs from regular Chrome | Headless Shell does not fully match regular Chrome behavior. | Reproduce in both modes; use regular headless mode if a required behavior is incompatible. |
| Startup timeout despite healthy pages | Cold startup is slow, system is resource constrained, or browser install/version is mismatched. | Measure startup, validate the bundled browser pairing, inspect process logs, and tune timeout as a failure limit. |
| PDF is slow or typography changes | Fonts are still loading, or the print layout is expensive. | Confirm font readiness and required layout; preserve waitForFonts unless correctness is proven without it. |
| Performance degrades as concurrency rises | CPU, memory, network or remote site limits are saturated. | Lower concurrency, track queue time and resource use, then increase gradually based on observed results. |
9. Performance, reliability, and cost in production
Track throughput alongside success rate, retry count, timeout rate, queue time, output size, and browser memory. Set explicit time budgets for startup, navigation, readiness, and capture so a slow stage is visible. Use bounded retries for transient failures, with backoff; retrying every failure immediately can overload your worker or the target site. Close pages in finally blocks and close browsers during graceful shutdown.
Browser automation cost includes compute, memory, browser installation and maintenance, and engineering time spent on version compatibility and failures. Measure resource use at your target concurrency and include cold starts, not just the fastest warm case. If you run in constrained containers, verify that the browser can start and that the selected mode behaves correctly there. Avoid treating a longer timeout as a capacity plan.
Or skip the browser setup
If your task is simply to get a website screenshot, ScreenshotNeo provides a screenshot API and MCP server. One GET request returns an image or PDF; the API supports PNG, JPEG and WebP. See the ScreenshotNeo API docs for options.
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}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo removes cookie banners, newsletter popups and chat widgets before the shot; each cleanup step can be turned off. Bot checks, blank pages and failed loads are never billed, and response headers indicate the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get 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 for 1,000 free screenshots a month, with no card.
FAQ
Does headless: 'shell' always make Puppeteer faster?
No. Puppeteer describes it as more performant for a particular class of automation, but offers no universal workload-specific speedup. Measure it and verify behavior.
Should I disable images to speed up page loads?
Only if images are irrelevant to the task. Blocking them changes screenshot fidelity and can alter layout. Compare outputs against the requirements.
Will a higher launch timeout improve performance?
No. It changes how long startup may take before Puppeteer reports failure. Use it to fit a measured startup envelope, not as an optimization.
Can I use Puppeteer’s timing on one website to predict another?
No. Site code, server response, third-party requests, page size, and readiness conditions vary. Benchmark a representative URL set from your own workload.


