Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Slow Puppeteer jobs usually have a specific cause: duplicated readiness waits, intercepted requests that never finish, or a cache policy that does not match the workload. Fixing the cause is safer than adding larger timeouts. The three techniques below—locators, selective request interception, and deliberate cache verification—come from Puppeteer’s documented behavior. Measure one representative workflow before and after each change; the documentation describes mechanics, not a universal speedup.
Start with a repeatable measurement
Choose a workflow that represents production: the same URL or URLs, authentication state, viewport, browser version, data set, and number of repetitions. Record total wall-clock time and the duration of individual phases such as navigation, waiting, clicking, typing, and extraction. Run enough repetitions to see whether a change is consistent, and keep cold-cache and warm-cache runs separate.
- Use the same Puppeteer and Chromium versions for both runs. The cited documentation displays versions from 25.9.0 through 25.12.0; API behavior can change between releases.
- Log timestamps around every major operation rather than only timing the whole script.
- Record failures, retries, and page responses as well as successful duration. A faster run that is less reliable is not an optimization.
- Do not use
slowMowhile measuring. Puppeteer documents it as an intentional delay for debugging, not a performance setting.
Puppeteer’s debugging guide covers browser inspection, console capture, protocol-traffic logging, and diagnostics for pending protocol calls. Use those tools to identify the slow operation before changing code: Puppeteer debugging guide.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Technique 1: replace duplicate waits with a locator
Puppeteer recommends locators for selecting and interacting with elements. A locator waits for action preconditions—including being in the viewport, visible, enabled where relevant, and stable across consecutive animation frames—and retries when an action fails because the element is not ready. When those conditions are what your workflow needs, one locator action can replace a separate wait followed by an interaction: Page interactions guide.
#1 Best Overall
The slower pattern
await page.waitForSelector('button[data-test="save"]', { visible: true });
const button = await page.$('button[data-test="save"]');
await button.click();
This sequence performs selection and readiness work in separate layers. It can also leave you holding an ElementHandle while the page rerenders. If you do use a handle, dispose of it after the operation.
The locator version
const save = page.locator('button[data-test="save"]');
await save.click();
The locator waits for the conditions required by click(). It does not mean every page-specific condition is covered: a button may be visible while an application is still loading data, for example. Add a targeted assertion or wait for that application state rather than stacking arbitrary sleeps.
When a locator is not a drop-in replacement
- If you need to wait for a page-specific state, such as a status label changing to “Saved,” wait for that state explicitly after the click.
- If you need a one-time DOM snapshot, an
ElementHandlemay be appropriate; release it when finished. - If an element is inside a frame or shadow root, create the locator from the appropriate frame or scope.
- If the selector matches multiple elements, make the locator unambiguous instead of relying on whichever node happens to be found first.
Understand what waitForSelector actually does
page.waitForSelector() waits for a selector to appear and resolves immediately when it is already present. Its documented default timeout is 30 seconds. It does not automatically retry the interaction you perform after it returns an ElementHandle: Page.waitForSelector API and WaitForSelectorOptions API.
Recommended Free Tools
await page.waitForSelector('#results', { timeout: 10_000 });
const results = await page.$$eval('#results li', nodes =>
nodes.map(node => node.textContent?.trim())
);
Use a shorter, condition-specific timeout when ten seconds is the real contract, and handle the resulting timeout as an expected failure. Avoid this pattern:
await page.waitForSelector('#submit');
await new Promise(resolve => setTimeout(resolve, 3000));
await page.click('#submit');
The fixed delay adds time even when the page is ready. Replace it with a locator action or a wait for the exact state that justifies the delay.
Technique 2: intercept requests only when you have a clear target
Request interception can modify, abort, or continue network requests. It can be useful for a known, unnecessary resource class, but enabling it changes request scheduling: Puppeteer’s guide says every request stalls until a handler continues it, responds to it, aborts it, or the browser completes it from cache. A handler that forgets to resolve one request can make navigation appear hung. Read the request interception guide and Page.setRequestInterception API.
A safe, narrow handler
await page.setRequestInterception(true);
page.on('request', request => {
// Another listener may already have resolved this request.
if (request.isInterceptResolutionHandled()) return;
if (request.resourceType() === 'image' && request.url().includes('/tracking/')) {
request.abort();
return;
}
request.continue();
});
The guard matters when multiple listeners or libraries are attached. Every branch resolves the request. Keep the rule specific: blocking all images, scripts, or fonts is not automatically faster and can change layout, application behavior, or the very data your test is meant to exercise.
Compare interception against the same workflow without it
- Run the representative workload with interception disabled and save timings and failure counts.
- Enable interception for one resource class that you can justify, such as a known analytics endpoint irrelevant to the test.
- Run the same number of cold and warm repetitions.
- Check page correctness, console errors, network failures, and screenshots or extracted data.
- Keep the change only if the avoided work outweighs interception overhead for this workload.
Disable interception when the workflow is complete if the same page object continues to perform ordinary browsing:
await page.setRequestInterception(false);
Technique 3: preserve useful browser caching
Puppeteer’s Page.setCacheEnabled() reference states that browser caching is enabled by default and that the method toggles whether requests ignore the cache: Page.setCacheEnabled API. A codebase may nevertheless disable it globally, or an interception handler may prevent normal cache behavior.
// Make the intended policy explicit for repeated page work.
await page.setCacheEnabled(true);
await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.reload({ waitUntil: 'domcontentloaded' });
Do not claim that this always accelerates a run. A first visit has little reusable data, cache headers may forbid reuse, and production may intentionally require fresh responses. Measure the same sequence under the same cache state:
Rank #3
- Cold run: a new context or otherwise controlled empty cache, if that matches a first-visit user.
- Warm run: repeated navigation in the same context, if users revisit assets.
- Fresh-data run: the cache policy your application requires for correctness.
Keep these categories separate in reports. Calling a warm-cache result a general page-load time hides the condition that produced it.
Free tools Windows power users keep installed
One-click scans. No signup required.
Combine the techniques without hiding the bottleneck
These changes address different layers and should not be enabled as a bundle without measurement. A locator can remove duplicate waits; interception can add a stall to every request; cache preservation can help only when repeated requests are reusable. Apply one change, inspect correctness, then compare.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setCacheEnabled(true);
const started = performance.now();
await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
await page.locator('input[name="email"]').fill('[email protected]');
await page.locator('input[name="password"]').fill(process.env.PASSWORD);
await page.locator('button[type="submit"]').click();
await page.locator('[data-test="dashboard"]').wait();
console.log(`workflow: ${(performance.now() - started).toFixed(0)} ms`);
await browser.close();
Use environment variables or a secret store for credentials. The example’s locator waits do not replace an application-specific readiness check; the dashboard marker is that check.
Troubleshooting slow or stuck runs
“Navigation never finishes” after interception is enabled
Cause: a request path does not call continue(), respond(), or abort(), or a second handler attempts to resolve an already handled request. Fix: add a final continue(), guard with isInterceptResolutionHandled(), and log each intercepted URL and resolution while diagnosing.
“Click intercepted” or repeated locator retries
Cause: an overlay, animation, disabled control, or unstable layout means the locator’s action preconditions are not met. Fix: wait for the real overlay-dismissed or application-ready state, remove an unexpected popup in test setup, and inspect the page rather than adding a long sleep.
Rank #4
“Timeout exceeded” from waitForSelector
Cause: the selector never appears, appears in a different frame, or the default 30-second timeout is unsuitable. Fix: verify the selector and frame, choose a timeout matching the operation, and capture HTML, console output, and a screenshot at failure.
Warm runs are not faster
Cause: caching may be disabled, responses may be non-cacheable, a new browser context may be created for every iteration, or interception may interfere with normal handling. Fix: confirm setCacheEnabled(true), compare the same context and navigation sequence, and inspect response headers and interception logs.
Runs became faster but results changed
Cause: an intercepted script, image, font, or API request was required for rendering or data. Fix: restore the request, narrow the rule, and treat correctness and failure rate as part of the performance result.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your goal is a clean screenshot rather than interactive browser control, ScreenshotNeo provides a single screenshot API request. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers.
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)
open("shot.webp", "wb").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}`);
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Its plans include 1,000 screenshots per month free without a card; paid plans start at $5 for 3,000 screenshots. See the ScreenshotNeo documentation, then sign up for the free plan.
FAQ
Should I increase Puppeteer’s default timeout first?
No. A larger timeout changes how long a failure takes to surface; it does not make a ready page faster. Identify the missing condition or slow operation first.
Best Value
Can I leave request interception enabled for every test?
Only if every request is resolved and your measurements show a benefit for the workload. Interception imposes per-request handling overhead and can alter page behavior.
Does enabling cache make tests unreliable?
It can if a test requires fresh server responses. Choose cold, warm, or fresh-data conditions deliberately and document that choice rather than treating one cache state as universally correct.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsAre locators guaranteed to reduce runtime?
No. They remove redundant waiting when their readiness conditions match the task. If the page itself is slow, a locator correctly waits for that slow condition.
Frequently Asked Questions
How do I know which technique helped?
Time the same representative workload before and after one change at a time, separating cold-cache and warm-cache runs and recording failures as well as duration.
What should I log when a run hangs?
Log navigation milestones, intercepted URLs and their resolutions, console errors, pending protocol calls, and the selector or locator currently being awaited.
Quick Recap
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.

