Browser pane: hidden tabs never receive requestAnimationFrame, permanently stalling Next.js (pages router) dev hydration

Status Fixed / completed
Reported on v2.1.170
Maintainer reply None cached
Activity 1 comment · opened Aug 5, 2026 · closed Aug 25, 2026

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: pageBootraphydrate({ 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

  1. preview_start a Next.js 14 pages-router next dev server; a tab opens.
  2. Background the tab (tabs_create + front the new tab), or use the session-start window before any screenshot has been taken.
  3. navigate the hidden tab to any page (hard navigation).
  4. 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.

  1. Front the tab (tabs_select) or take a screenshot of 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 dev appears 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_page return garbage (unlaid-out textContent) 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)

  1. Deliver occasional frames (or at least run rAF callbacks) for tabs the pane keeps hidden — Chromium's headless Page.captureScreenshot frame-pump behavior shows the machinery already exists.
  2. Or: have navigate/read_page/get_page_text pump one frame the way screenshot implicitly does, so observation tools never report the pre-boot limbo state.
  3. At minimum: document that non-streamed pane tabs receive no rAF, and that a screenshot after each hard navigation is required for rAF-gated apps.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗