networkidle vs load vs domcontentloaded: Wait Strategies

Wait strategies decide when a headless browser treats a page as ready. What load, domcontentloaded, networkidle0 and networkidle2 mean in each tool.

By Simon Chiu · Updated

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

  • networkidle on 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.
  • domcontentloaded for 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 networkidle means "data drawn". A request can finish and the framework can render a frame later. Wait for the element when you can.

To try wait.for_selector on your own app without running a browser, get a free API key.

Frequently asked questions

What is the difference between networkidle0 and networkidle2?
Both are Puppeteer options. networkidle0 resolves when there have been no network connections for 500 ms. networkidle2 resolves when there have been at most two for 500 ms, which tolerates long-polling and analytics beacons.
Does Playwright have networkidle0?
No. Playwright has one networkidle value, which waits for no network connections for at least 500 ms. Playwright's docs discourage it for tests and recommend waiting on web-first assertions instead.
Is load slower than domcontentloaded?
It can never fire earlier. load waits for images, stylesheets and subframes as well as the parsed HTML. On a page with few resources the two are close together.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.