Skip to content

feat(render): networkidle2 in-flight wait, replacing the completion-reset idle probe#120

Merged
andylizf merged 1 commit into
mainfrom
feat/networkidle2-inflight-wait
Jul 16, 2026
Merged

feat(render): networkidle2 in-flight wait, replacing the completion-reset idle probe#120
andylizf merged 1 commit into
mainfrom
feat/networkidle2-inflight-wait

Conversation

@andylizf

Copy link
Copy Markdown
Contributor

Problem

The wait_network_idle probe ran in-page on a PerformanceObserver: every resource completion reset its 500ms quiet timer. Analytics beacons / polling complete more often than every 500ms on much of the open web, so those pages always rode the 12s hard cap.

Measured live on a page firing a beacon every 200ms:

wall clock
old in-page probe 15.6s (12s cap + overhead)
this PR 4.0s

Change

Count in-flight requests instead, Python-side over CDP Network.* events — Puppeteer's networkidle2 semantics, the industry default for capturing arbitrary pages (tolerate ≤2 pending requests for 500ms, hard-capped at 12s as before):

  • requestWillBeSent opens, loadingFinished/loadingFailed closes; readiness = loaded AND ≤2 in flight for 500ms.
  • Persistent connections (long-poll; websockets use different CDP methods and are never counted) no longer block. SPA hydration bursts (>2 concurrent) still hold capture open.
  • The quiet clock runs from navigation, so a page already quiet when load fires proceeds immediately — static pages now pay ~0 where the old probe always paid ≥500ms.
  • The watcher owns the websocket between Page.navigate and the in-page height probe (per-page CDP flow is sequential; _cdp_send discards event frames, so the watcher must be the reader while the page settles). An initial raw document.readyState probe catches pages that loaded before it attached.

_NetIdleState is pure and clock-injected; tests cover tolerance, load gating, beacon timing, the hard cap, and the pre-loaded-page path via a fake websocket (8 new tests, no Chrome needed).

Verified end-to-end (real Chrome)

  • example.com with/without the flag: complete tiles, ~0.5s idle overhead on a single-request page.
  • Local beacon page (200ms interval): 15.6s → 4.0s, tiles complete.

Not in this PR

The turbo (fast_cdp) path still bypasses the idle wait — it needs the same watcher wired into its _Conn event loop, and I can't benchmark turbo on this machine (/dev/shm + patched headless_shell). The silent fallback is now an explicit log line; turbo idle support is a scoped follow-up.

…eset probe

The wait_network_idle probe ran in-page on a PerformanceObserver: every
resource *completion* reset its 500ms quiet timer. Analytics beacons and
polling complete more often than every 500ms on much of the open web, so
those pages always rode the 12s hard cap — measured live on a page with
200ms beacons: 15.6s with the old probe, 4.0s with this change.

Count in-flight requests instead, Python-side over CDP Network events
(Puppeteer's networkidle2 semantics, the industry default for capturing
arbitrary pages): Network.requestWillBeSent opens a request,
loadingFinished/loadingFailed closes it, and readiness means "page loaded
AND <=2 requests in flight for 500ms", capped at LOAD_TIMEOUT_MS as
before. Persistent connections (long-poll, websockets — different CDP
methods, never counted) no longer block; SPA hydration bursts (>2
concurrent) still hold capture open. The quiet clock runs from navigation,
so a page that is already quiet when `load` fires proceeds immediately —
static pages now pay ~nothing where the old probe always paid >=500ms.

Mechanics: the watcher owns the websocket between Page.navigate and the
in-page height probe (the per-page CDP flow is sequential), because
_cdp_send discards event frames while awaiting a response. An initial
raw document.readyState probe catches pages whose load event fired before
the watcher attached. _readiness_expr drops its in-page idle step and
parameter; Network.enable in _setup_page is now load-bearing for the
watcher and commented as such.

The turbo path still bypasses the idle wait (it has no watcher yet) — now
with an explicit log line instead of a silent fallback. _NetIdleState is
pure and clock-injected; tests cover the tolerance, load gating, beacon
timing, the hard cap, and the pre-loaded-page probe with a fake websocket.
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
web Ready Ready Preview, Comment Jul 16, 2026 10:36am

@andylizf
andylizf merged commit 1ee3d8e into main Jul 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant