A wait strategy is the rule a headless browser uses to decide that a page is ready to capture: a browser lifecycle event such as domcontentloaded or load, a quiet network (networkidle), the presence of a specific element, or a fixed delay.
Choosing it is the most common fix for blank, half-loaded or spinner-filled screenshots. This entry is part of the screenshot glossary. For a measured, step-by-step guide to the RenderScreenshot options, read the wait strategies guide.
What it means in plain terms
"The page has loaded" is not one moment. The HTML arrives first, then stylesheets and images, then scripts fetch data and draw it, then fonts swap in and animations settle. Each wait strategy picks one of those moments. Pick too early and you capture an empty shell. Pick a moment that never comes, such as a quiet network on a page with a live chat, and you wait until the timeout.
The timeline
flowchart LR A["Navigation starts"] --> B["HTML parsed<br/><b>domcontentloaded</b>"] B --> C["Images, CSS, iframes done<br/><b>load</b>"] C --> D["JS fetches data, renders"] D --> E["No requests for 500 ms<br/><b>networkidle</b>"] D --> F["Your element appears<br/><b>wait for selector</b>"]
domcontentloaded and load are real DOM events the page itself fires. networkidle is not a browser event: the automation library invents it by watching requests. A selector wait is the only strategy that knows what your content looks like.
How the names map across tools
| Moment | Puppeteer waitUntil |
Playwright waitUntil |
RenderScreenshot wait.until |
|---|---|---|---|
| Response received, before parsing | not available | commit |
not available |
| HTML parsed | domcontentloaded |
domcontentloaded |
domcontentloaded |
| Subresources loaded | load (default) |
load (default) |
load (default) |
| At most 2 connections for 500 ms | networkidle2 |
not available | not available |
| 0 connections for 500 ms | networkidle0 |
networkidle |
networkidle |
| An element reaches a state | page.waitForSelector() |
locator.waitFor() |
wait.for_selector + for_selector_mode |
The trap in this table: networkidle in Playwright and RenderScreenshot means Puppeteer's networkidle0, not networkidle2. Code ported from Puppeteer that used networkidle2 to survive a polling page will hang until the timeout after the port.
Puppeteer: compare the four options on any URL
import puppeteer from 'puppeteer'; const url = process.argv[2] ?? 'https://example.com'; const strategies = ['domcontentloaded', 'load', 'networkidle2', 'networkidle0']; const browser = await puppeteer.launch(); try { for (const waitUntil of strategies) { // A fresh context per run, so the HTTP cache from the last run does not skew the time. const context = await browser.createBrowserContext(); const page = await context.newPage(); const start = performance.now(); await page.goto(url, { waitUntil, timeout: 30_000 }); const ms = Math.round(performance.now() - start); const images = await page.$$eval('img', (imgs) => imgs.filter((i) => i.complete).length + '/' + imgs.length); console.log(`${waitUntil.padEnd(17)} ${String(ms).padStart(6)} ms images loaded ${images}`); await context.close(); } } finally { await browser.close(); }
Run it against your own pages. On example.com, a page with no images and no scripts, both network-idle options came back roughly a second after navigation started while the two DOM events took a few hundred milliseconds. The idle window itself costs at least 500 ms, so every networkidle capture pays that on top of the real load time.
Playwright: wait for the content, not the network
import { chromium } from 'playwright'; const browser = await chromium.launch(); try { const page = await browser.newPage(); await page.goto(process.argv[2] ?? 'https://example.com', { waitUntil: 'domcontentloaded' }); // Then wait for the thing you need, not for the network. await page.locator('p').first().waitFor({ state: 'visible' }); await page.evaluate(() => document.fonts.ready); await page.screenshot({ path: 'ready.png' }); console.log('captured after first paragraph visible and fonts ready'); } finally { await browser.close(); }
document.fonts.ready covers a gap none of the events handle: web fonts that finish after the content is visible.
Tested on 2026-10-15 with Node 22.18.0, [email protected] (Chrome 148, run with headless: 'shell') and [email protected] (Chromium 156).
RenderScreenshot
All wait controls live in the wait object of a POST request. They run in order: until, then for_selector, then delay, all inside timeout (seconds, default 30, maximum 60):
{ "url": "https://app.example.com/dashboard", "wait": { "until": "domcontentloaded", "for_selector": "[data-loaded='true']", "for_selector_mode": "visible", "delay": 300, "timeout": 45 } }
With the Node.js SDK the same options are .waitFor('domcontentloaded'), .waitForSelector("[data-loaded='true']") and .delay(300). GET URLs accept only delay and timeout, so use POST when you need until or a selector. The wait parameter reference lists every mode.
Common mistakes
networkidleon pages that never go quiet: analytics beacons, chat widgets, websockets, polling dashboards. The wait ends at the timeout every time.- A long fixed delay instead of a condition. It is slow on fast pages and still too short on slow ones.
domcontentloadedfor visual output. Images and fonts are often missing.- Ignoring lazy loading. Images below the fold do not load until scrolled into view, whatever you wait for.
- Assuming
networkidlemeans "data drawn". A request can finish and the framework can render a frame later. Wait for the element when you can.
Related terms
To try wait.for_selector on your own app without running a browser, get a free API key.