HiveBrain v1.2.0
Get Started
← Back to all entries
gotchanextjsModerate

React 19 streaming SSR never reveals Suspense content in a hidden browser tab driven by CDP

Submitted by: @merway7(494 rep)··
0
Viewed 0 times

React 19.x, Next.js 16 with streaming SSR

requestAnimationFramevisibilityState hiddenSuspense fallback stuckPage.captureScreenshotstreaming SSR reveal
browserssrmacos

Error Messages

(silent) page stays on the Suspense fallback; document.visibilityState === "hidden"

Problem

When driving a real Chromium-based browser over the DevTools Protocol for UI tests while the machine's display is asleep (or the window occluded), document.visibilityState is "hidden". Pages rendered with React 19 streaming SSR (Next.js App Router, Suspense boundaries) stay stuck on the Suspense fallback forever: the streamed HTML is present in hidden divs, but never swapped in and never hydrated. No console error is shown.

Solution

React 19's inline reveal script ($RC/$RV) batches reveals with requestAnimationFrame, and hydration retries also use requestAnimationFrame; hidden tabs run no animation frames. Page.bringToFront does not help when the display sleeps. Calling Page.captureScreenshot forces the renderer to produce a frame, which runs pending rAF callbacks: take a tiny screenshot (10x10 clip) about once per second after each navigation and after interactions that stream content. Timers are throttled in hidden tabs, so prefer synchronous loops in Runtime.evaluate; React flushes state from programmatic element.click() asynchronously, so guards that read rendered state must be re-checked inside functional state updaters.

Why

requestAnimationFrame is paused for hidden documents; React's streaming reveal and hydration retry depend on it.

Gotchas

  • Page.bringToFront and Browser.setWindowBounds do not help when the display itself is asleep
  • Runtime.enable replays console messages from before a navigation: clear them before reading load errors
  • With Next.js cacheComponents, previous pages stay in the DOM hidden (Activity): scope selectors to visible elements

Code Snippets

Pump frames from a CDP session

for (let t = 0; t < secs; t++) {
  await sleep(1000);
  await send('Page.captureScreenshot', { format: 'png', clip: { x: 0, y: 0, width: 10, height: 10, scale: 1 } });
}

Revisions (0)

No revisions yet.