Menu
Sign up

Puppeteer: wait for the page to load — load, networkidle0, networkidle2 or a selector? Measured

Measured with Puppeteer 25.7: what load, domcontentloaded, networkidle0/2 and waitForSelector wait for, how long each took, and which one hung.

Published November 8, 2026 · 5 min read

"Wait until the page is loaded" sounds like one thing and is several. A page can have its HTML parsed, its images downloaded, its web fonts still arriving, its data still coming from an API, and an analytics connection that never closes — all at once. Puppeteer's waitUntil options each wait for a different one of those moments, and choosing the wrong one gives you either a half-empty screenshot or a timeout. We measured what each one catches on the same test page.

How we measured. Puppeteer 25.7.0 in the official ghcr.io/puppeteer/puppeteer image, on 2026-11-08, against a local test page built to have every common delay: a web font served after 1.5 s, two images after 0.4 s, a lazy image far below the fold, a CSS animation, and data fetched by a script that arrives after about 2.1 s. A second version of the page also opened a server-sent events connection that never closes, as chat widgets and live dashboards do. Each strategy ran five times; the table shows the median.

What each strategy caught

Strategy Time Data from the script Web font Images (of 3)
goto waitUntil: 'domcontentloaded' 24 ms no no 0
goto waitUntil: 'networkidle2' 941 ms no no 2
goto waitUntil: 'load' 1,572 ms no yes 2
load + document.fonts.ready 1,555 ms no yes 2
goto waitUntil: 'networkidle0' 2,645 ms yes yes 2
load + waitForSelector('#data') 2,123 ms yes yes 2
domcontentloaded + waitForSelector + fonts.ready 2,180 ms yes yes 2

With the connection that never closes:

Strategy Time Result
networkidle0 15,000 ms timed out
networkidle2 2,061 ms font, but no data
load + waitForSelector('#data') 2,126 ms everything

Four findings:

  1. domcontentloaded is far too early for a screenshot. 24 ms in, nothing but the HTML text is there.
  2. load waited for the web font but not for the data. Fonts and images are part of the page's own load; a fetch() started by a script is not.
  3. networkidle2 returned before both the font and the data on the first page. "At most two connections open" was true while the font and the API request were the two.
  4. networkidle0 is the only option that caught everything without a selector — and the only one that hung. One open connection, and it never returns.

No strategy loaded the lazy image: below the fold, the browser never requested it. That needs scrolling, not waiting.

What the options mean

load and domcontentloaded wait for the browser events of the same names; Puppeteer's documentation defines the two network options:

  • load — the window's load event: the HTML and the resources it references (stylesheets, images, fonts in use).
  • domcontentloaded — the HTML has been parsed; images and fonts may still be loading.
  • networkidle0 — "no more than 0 network connections for at least 500 ms".
  • networkidle2 — "no more than 2 network connections for at least 500 ms".

(PuppeteerLifeCycleEvent). The network options are heuristics: they guess "done" from silence on the network, which is why they are both too early (two slow requests count as idle) and too late (one endless connection never is).

The reliable pattern: wait for what you need

await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
await page.waitForSelector('#data', { timeout: 10_000 });   // the content that arrives later
await page.evaluate(() => document.fonts.ready);             // web fonts, if any are still loading
await page.screenshot({ path: 'page.png' });
  • waitForSelector is the most precise wait there is: it returns the moment the element you care about exists. Pick something that appears only when the content is ready — the data table, a product price, a chart's container with a "ready" class — not a placeholder.
  • document.fonts.ready resolves when the fonts the page uses have loaded. In our test load already covered the font, but a font requested later (by text inserted after load) is not covered by load.
  • Timeouts on everything. A selector that never appears should fail in seconds, not hang the job.

If you do not control the page and have no selector to wait for, networkidle2 plus a short fixed pause is a reasonable fallback — but check the result, and expect it to miss content on slow APIs.

Other waits worth knowing

  • page.waitForNetworkIdle({ idleTime: 500 }) — the networkidle0 rule after an action such as a click, not only after navigation.
  • page.waitForFunction(() => window.appReady === true) — when your own app can signal that it has rendered.
  • A fixed delay (await new Promise(r => setTimeout(r, 1000))) — the last resort, for animations that must finish. page.waitForTimeout is gone: in Puppeteer 25.7 it is undefined.
  • Lazy images: scroll through the page, or set loading="eager" on them before the shot. Our full-page screenshot guide measured how many load and why.
  • Animations: freeze them with CSS before the shot, or a spinner is caught mid-turn (visual regression guide).

How our screenshot API waits

If you take screenshots through Shareglint instead of running Puppeteer yourself, the same choices are parameters. Every screenshot first waits for DOMContentLoaded, then for the load event (up to a few seconds), then for the network to be quiet for 500 ms with at most two requests open (up to 2.5 s) — so an endless connection costs a few seconds, not the whole job. On top of that:

You need Parameter
Content that arrives later wait_for: a CSS selector to wait for
An animation or chart to finish delay: extra seconds to wait
Lazy images in a full-page shot lazy_load: scrolls through the page first
No cookie banner block_cookie_banners
curl -X POST "https://shareglint.com/v1/screenshots?wait=true" \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/dashboard", "wait_for": "#data", "full_page": true, "lazy_load": true }'

A viewport screenshot uses 1 render of the monthly quota, a full page 2 (screenshot API). For pages behind a login or on localhost, keep using Puppeteer locally with the pattern above.

FAQ

How do I wait for the page to fully load in Puppeteer?

There is no single "fully loaded" event. Wait for the thing you need: goto with waitUntil 'load' for the page's own resources, then waitForSelector for content that arrives later from a script or request, and document.fonts.ready for web fonts. In our test that combination caught everything in about 2.1 s.

What is the difference between networkidle0 and networkidle2?

networkidle0 waits until no network connection has been open for 500 ms; networkidle2 allows up to two. In our test networkidle2 returned after 0.94 s with the data and web font still missing, and networkidle0 never returned on a page that kept one connection open — it timed out after 15 s.

Why does waitUntil networkidle0 time out?

The page keeps a connection open — server-sent events, a WebSocket, a long-polling chat widget, analytics beacons. The network is never idle for 500 ms, so the wait runs into the timeout. Use load plus waitForSelector for the content you need instead.

Does waitUntil load wait for lazy images?

No. Images with loading="lazy" below the visible area are not requested until they come near the viewport, so no wait condition loads them. In our test the lazy image stayed unloaded with every strategy; scroll the page or set loading to eager first.