Skip to content

Fix dev mode hydration failure when page is served from HTTP cache#92892

Merged
unstubbable merged 1 commit intocanaryfrom
hl/bfcache-issue
Apr 16, 2026
Merged

Fix dev mode hydration failure when page is served from HTTP cache#92892
unstubbable merged 1 commit intocanaryfrom
hl/bfcache-issue

Conversation

@unstubbable
Copy link
Copy Markdown
Contributor

@unstubbable unstubbable commented Apr 16, 2026

PR #88182 introduced an experimental option to use Cache-Control: no-cache instead of no-store in dev mode, and PR #91503 made it the default. With no-cache, browsers may serve the page from HTTP cache on back-forward navigation or tab duplication. The HTML, including the inline RSC payload, is restored from cache and all scripts re-execute.

In dev mode, React's Flight client uses a debug channel (a WebSocket-backed stream delivering component debug info) that adds dependencies to model chunk initialization. On a fresh page load, the WebSocket delivers this data and the dependencies resolve normally. On HTTP cache restore, however, the bootstrap script re-executes and creates a new debug channel stream, but the WebSocket doesn't re-send debug data for the cached payload's request ID. The debug dependencies are never fulfilled, blocking the entire model tree from initializing, so hydrateRoot is never called and the page loses all interactivity.

This is dev-only — production builds have no debug channel, so there are no stuck dependencies and no issue.

The fix buffers debug channel chunks in memory as they flow through the TransformStream in getOrCreateDebugChannelReadableWriterPair. Once all chunks have been received, the buffer is eagerly persisted to sessionStorage. When the page detects it was served from HTTP cache (via PerformanceNavigationTiming.transferSize === 0), createDebugChannel restores the debug data from sessionStorage and replays it as a synthetic ReadableStream instead of expecting it from the WebSocket. If the restore fails (e.g., quota exceeded during the earlier write, or the entry was overwritten by another page), the page falls back to location.reload() to get a fresh page from the server.

A regression test is included that navigates to an external page and verifies interactivity is preserved after clicking the browser back button. Tab duplication (which triggers the same HTTP cache restore) cannot be simulated in Playwright and was verified manually.

fixes #92238
fixes #91982
fixes #92687

PR #88182 introduced an experimental option to use `Cache-Control:
no-cache` instead of `no-store` in dev mode, and PR #91503 made it the
default. With `no-cache`, browsers may serve the page from HTTP cache on
back-forward navigation or tab duplication. The HTML, including the
inline RSC payload, is restored from cache and all scripts re-execute.

In dev mode, React's Flight client uses a debug channel (a
WebSocket-backed stream delivering component debug info) that adds
dependencies to model chunk initialization. On a fresh page load, the
WebSocket delivers this data and the dependencies resolve normally. On
HTTP cache restore, however, the bootstrap script re-executes and
creates a new debug channel stream, but the WebSocket doesn't re-send
debug data for the cached payload's request ID. The debug dependencies
are never fulfilled, blocking the entire model tree from initializing,
so `hydrateRoot` is never called and the page loses all interactivity.

This is dev-only — production builds have no debug channel, so there are
no stuck dependencies and no issue.

The fix buffers debug channel chunks in memory as they flow through the
`TransformStream` in `getOrCreateDebugChannelReadableWriterPair`. Once
all chunks have been received, the buffer is eagerly persisted to
`sessionStorage`. When the page detects it was served from HTTP cache
(via `PerformanceNavigationTiming.transferSize === 0`),
`createDebugChannel` restores the debug data from `sessionStorage` and
replays it as a synthetic `ReadableStream` instead of expecting it from
the WebSocket. If the restore fails (e.g., quota exceeded during the
earlier write, or the entry was overwritten by another page), the page
falls back to `location.reload()` to get a fresh page from the server.

A regression test is included that navigates to an external page and
verifies interactivity is preserved after clicking the browser back
button. Tab duplication (which triggers the same HTTP cache restore)
cannot be simulated in Playwright and was verified manually.
@nextjs-bot
Copy link
Copy Markdown
Contributor

nextjs-bot commented Apr 16, 2026

Tests Passed

@unstubbable unstubbable marked this pull request as ready for review April 16, 2026 15:11
@unstubbable unstubbable merged commit 67f4f5e into canary Apr 16, 2026
465 of 486 checks passed
@unstubbable unstubbable deleted the hl/bfcache-issue branch April 16, 2026 16:36
@github-actions github-actions Bot added the locked label May 1, 2026
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators May 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants