Browser pane: hidden tabs never receive requestAnimationFrame, permanently stalling Next.js (pages router) dev hydration
Environment
- Claude Code 2.1.170, desktop app, macOS (Darwin 25.5.0)
- Built-in Browser pane tools (
preview_start/navigate/javascript_tool/computer) - App under test: Next.js 14.2.35 (pages router) + React 18.3.1,
next dev
Summary
The Browser pane holds a tab at document.visibilityState === 'hidden' whenever it isn't the fronted, actively-streamed tab — including the fronted tab at session start, before the first screenshot activates the stream. In that state the tab's task queues run, but requestAnimationFrame callbacks are never invoked and the document has no layout.
Next.js's pages-router dev bootstrap awaits an rAF-gated FOUC guard before hydrating: pageBootrap → hydrate({ beforeRender: displayContent }), where displayContent (next/dist/client/dev/fouc.js) returns a promise resolved inside requestAnimationFrame (for a top-level window). No frame ⇒ that promise never resolves ⇒ React never hydrates. The page executes its module scripts and then sits dead indefinitely.
The failure is invisible-by-default and actively misleading for agents: the page "loads" (readyState: 'complete', globals from module scope exist) but never mounts, and the pane's text tools make it worse — with no layout, innerText degrades to textContent, so get_page_text returns the raw __NEXT_DATA__ JSON. An agent session burned considerable time inventing store-dispatch workarounds for what was actually a starved rAF (and concluded the pane was undrivable — our project docs steered agents to real Chrome because of this).
Reproduction
preview_starta Next.js 14 pages-routernext devserver; a tab opens.- Background the tab (
tabs_create+ front the new tab), or use the session-start window before any screenshot has been taken. navigatethe hidden tab to any page (hard navigation).- Wait 10+ seconds, then probe with
javascript_tool:
({
vis: document.visibilityState, // "hidden"
readyState: document.readyState, // "complete"
bootedGlobals: typeof window.next, // scripts ran
reactMounted: !!document.querySelector('#__next')?.childElementCount, // false — stuck
text: document.body.innerText.slice(0, 60), // raw __NEXT_DATA__ JSON
})
The page stays in this state indefinitely.
- Front the tab (
tabs_select) or take ascreenshotof it — it boots instantly.
Isolating the mechanism
Scheduled in the hidden tab, then waited 5 s:
| primitive | fires while hidden? |
|---|---|
| setTimeout(…, 300) | ✅ |
| MessageChannel post | ✅ |
| requestAnimationFrame | ❌ (fired later, during a screenshot of the still-hidden tab) |
So this is not tab freezing/throttling — only the rendering pipeline is stopped, and exactly one class of callback starves. A screenshot call pumps a single frame even on a hidden tab (the pending rAF fired during it, with visibilityState still "hidden" after), which is why screenshots "magically" unstick the page.
Note Next's own comment in fouc.js anticipates this hazard for iframes ("there are no guarantees that the callback will actually be called, which could stall the promise") and falls back to setTimeout there — but not for a hidden top-level page, which is what the pane creates.
Impact
- Any Next.js pages-router app under
next devappears completely dead in the pane unless the agent happens to screenshot after each hard navigation. Other frameworks with rAF-gated boot paths would hit the same wall. get_page_text/read_pagereturn garbage (unlaid-outtextContent) for the stuck page, sending agents down false debugging trails (in our case: sessions diagnosed app bugs, minted fresh auth tokens, and shipped a no-op workaround snippet).
Suggested fixes (any of these would do)
- Deliver occasional frames (or at least run rAF callbacks) for tabs the pane keeps hidden — Chromium's headless
Page.captureScreenshotframe-pump behavior shows the machinery already exists. - Or: have
navigate/read_page/get_page_textpump one frame the wayscreenshotimplicitly does, so observation tools never report the pre-boot limbo state. - At minimum: document that non-streamed pane tabs receive no rAF, and that a
screenshotafter each hard navigation is required for rAF-gated apps.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗