Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use an async function and put await inside a normal for...of (or indexed for) loop. The next iteration starts only after the awaited operation in the current iteration settles:
async function processItems(page, items) {
for (const item of items) {
await processItem(page, item);
}
}
This is ordinary JavaScript serial control flow; Puppeteer does not need a special loop API. Use Promise.all only when iterations are genuinely independent and can safely overlap.
Serialize Puppeteer iterations with await
An await expression suspends the current asynchronous function until its promise fulfills or rejects. Because the expression is in the loop body, the loop cannot advance while that operation is pending.
Free tools Windows power users keep installed
One-click scans. No signup required.
import puppeteer from 'puppeteer';
async function processItem(page, item) {
await page.goto(item.url, { waitUntil: 'domcontentloaded' });
return page.title();
}
async function main() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const items = [
{ url: 'https://example.com/one' },
{ url: 'https://example.com/two' },
{ url: 'https://example.com/three' }
];
try {
for (const item of items) {
const title = await processItem(page, item);
console.log(item.url, title);
}
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Here, navigation and title extraction for item two cannot begin until item one has completed. This pattern is appropriate when work depends on an earlier result, uses one page’s mutable state, or must preserve side-effect order.
#1 Best Overall
Why forEach is usually wrong
Array.prototype.forEach does not await an asynchronous callback. All callbacks are started without the loop waiting for their returned promises:
items.forEach(async item => {
await processItem(page, item);
});
console.log('This may run before any item finishes');
Use for...of, an indexed loop, or explicitly construct and await promises instead.
Indexed loops and collecting results
const titles = [];
for (let index = 0; index < items.length; index += 1) {
titles.push(await processItem(page, items[index]));
}
The result array is populated in processing order. If one operation rejects, execution leaves the loop unless you catch that error inside the iteration.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
When iterations may run concurrently
If every task is independent and can use separate browser contexts or pages, start them together and await the aggregate:
const results = await Promise.all(
items.map(item => processItemOnOwnPage(browser, item))
);
Promise.all fulfills after every supplied promise fulfills and returns values in input order, even if completion order differs. It rejects when any input rejects. Promises that were already started are not automatically canceled, so a rejection does not stop sibling browser work.
Rank #2
Do not share a stateful page casually
Concurrent calls against one page can race over URL, cookies, dialogs, form fields, and DOM state. A safer concurrent design creates a page per task:
async function processOnOwnPage(browser, item) {
const page = await browser.newPage();
try {
await page.goto(item.url, { waitUntil: 'domcontentloaded' });
return await page.title();
} finally {
await page.close();
}
}
Even with separate pages, consider the target site’s rate limits and your machine’s CPU, memory, and network capacity. Concurrency is a correctness decision first and a performance decision second.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteHandling failures while aggregating
To keep one failed item from rejecting the entire aggregate, convert each outcome to a value:
const outcomes = await Promise.all(items.map(async item => {
try {
return { item, value: await processOnOwnPage(browser, item) };
} catch (error) {
return { item, error: error.message };
}
}));
This records failures while allowing other tasks to finish. If failure should stop processing and preserve order, use the serial loop and catch at the boundary you choose.
Clicks that trigger navigation
Do not await a click and only then begin waiting for navigation. The navigation can start during the click, creating a race in which the wait is installed too late. Start both promises together:
await Promise.all([
page.waitForNavigation(),
page.click('a.next')
]);
Inside a serial loop, await this pair before advancing:
for (const selector of nextLinkSelectors) {
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.click(selector)
]);
await readCurrentPage(page);
}
If the click does not navigate, do not add a navigation wait merely to slow the loop. Wait for the state change that defines completion, such as a result element, URL change, or application-specific marker.
Waiting for elements is different from waiting for the previous iteration
A selector wait answers “is this element available?” It does not by itself serialize your outer loop. Puppeteer’s documented default timeout for page.waitForSelector() is 30 seconds; set a per-call timeout when a page needs a different limit.
await page.waitForSelector('.result', { timeout: 10000 });
Puppeteer recommends locators for selecting and interacting with elements. Locator actions wait for documented interaction preconditions, including visibility, enabled state, and a stable bounding box:
await page.locator('button.next').click();
Use a locator when the supported action itself expresses what must be ready. Use waitForSelector when you need an explicit readiness checkpoint before reading, evaluating, or performing another operation.
Rank #4
A complete serial example
async function processItems(page, items) {
for (const item of items) {
const selector = `[data-id="${CSS.escape(item.id)}"]`;
await page.locator(selector).click();
await page.waitForSelector('.result');
const text = await page.locator('.result').textContent();
console.log(item.id, text);
}
}
The selector, result condition, and whether a click navigates depend on the target site. If it navigates, replace the click with the paired waitForNavigation/click pattern.
page.evaluate() and loop sequencing
Puppeteer automatically awaits a promise returned by the function executed in the browser context:
const value = await page.evaluate(async () => {
const response = await fetch('/api/status');
return response.json();
});
That behavior only waits for the page-side function’s returned promise. Keep coordination between loop iterations in the Node.js function:
for (const item of items) {
const value = await page.evaluate(async id => {
const response = await fetch(`/api/items/${id}`);
return response.json();
}, item.id);
await save(value);
}
Timeouts, retries, and cleanup
Set timeouts deliberately
A wait that is too short produces false failures; an unlimited wait can stall a batch forever. Set navigation and selector limits appropriate to the site, and log the item being processed so a timeout is actionable.
page.setDefaultTimeout(15000);
page.setDefaultNavigationTimeout(30000);
Retry only retryable operations
A retry can help with transient navigation or network failures, but repeating a non-idempotent click may submit a form twice. Keep retries bounded and re-establish the page state before retrying.
Best Value
async function withRetry(operation, attempts = 3) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === attempts) break;
}
}
throw lastError;
}
Always close pages and browsers
Put cleanup in finally. This prevents a rejected iteration from leaving Chromium processes or pages running and consuming resources.
Common mistakes and fixes
- The next iteration starts immediately: the callback was launched with
forEachor promises were not awaited. Replace it withfor...ofand await the operation in the body. page.waitForNavigation()times out after a click: the wait was started after the click, or the click changes the DOM without navigation. Start both together, or wait for the actual DOM marker.- Results belong to the wrong item: concurrent tasks share one page. Serialize them or allocate one page/context per task.
waitForSelectortimes out: verify the selector, frame, visibility assumptions, and timeout. If the element is inside an iframe, obtain the correct frame before waiting.- A later task never runs after one error: an unhandled rejection exited the serial loop. Catch per item if you want to continue, or catch once at the outer boundary if fail-fast behavior is intended.
- Browser memory grows during a batch: close task pages in
finally, limit concurrency, and avoid retaining large page results unnecessarily.
Or skip the browser setup
If your goal is simply to capture pages rather than automate a stateful browser workflow, ScreenshotNeo provides a single HTTP request. Its clean-shot steps accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for all options. The same endpoint works from 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)
And 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. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Frequently Asked Questions
Does adding await to a loop make Chromium single-threaded?
No. It serializes the awaited JavaScript operations in that function. Chromium can still perform internal work, but your next iteration does not start until the awaited promise settles.
Should I use Promise.allSettled instead of Promise.all?
Use Promise.allSettled when you need every fulfillment and rejection outcome without failing at the first rejection. Use Promise.all when one failure should reject the aggregate.
Can I use a locator and waitForSelector together?
Yes, but avoid redundant waits. A locator action usually supplies the interaction preconditions; add an explicit selector wait when you need a separate readiness checkpoint or are about to read state.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchQuick 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.

