# 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 2026-11-08 · https://shareglint.com/en/guides/puppeteer-wait-for-page-load

"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](https://pptr.dev/api/puppeteer.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

```javascript
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 })`](https://pptr.dev/api/puppeteer.page.waitfornetworkidle)** — 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](https://shareglint.com/en/guides/puppeteer-full-page-screenshot) measured how many load and why.
- **Animations**: freeze them with CSS before the shot, or a spinner is caught mid-turn ([visual regression guide](https://shareglint.com/en/guides/visual-regression-screenshots)).

## 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` |

```bash
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](https://shareglint.com/en/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.
