[backport] Fix dev mode hydration failure when page is served from HTTP cache#93492
Merged
Conversation
Contributor
Tests PassedCommit: ba5ac8d |
…92892) 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
#93486) Follow-up to #92892, addressing the regression reported in #93136 (comment). That PR persisted the dev-mode debug channel chunks under a single shared `sessionStorage` key (`__next_debug_channel`) so the data could be replayed when a page is served from HTTP cache. The single-key design holds when the only navigations away from a Next.js page go to other origins, but it breaks the moment two Next.js documents share the same tab. When a user does an MPA navigation from page A to page B (a regular `<a href>` link to another Next.js route), B's persisted entry overwrites A's under the same key. Clicking back restores A from HTTP cache; A's bootstrap re-executes with its original `self.__next_r`, looks up `sessionStorage`, finds B's `requestId`, returns `undefined` from `restoreDebugChannelFromSessionStorage`, and falls back to `location.reload()`. Before the reload completes, the main RSC stream closes; the Flight client surfaces references to debug-channel chunks that block other model chunks — references the debug channel was supposed to deliver but never did — as a `Connection closed.` error, briefly flashing the error UI. The fix changes the storage key from `__next_debug_channel` to a per-document prefix `__next_debug_channel:${requestId}`. Each initial document persists into its own entry, so A's data survives B's load and the back-restore finds it. `requestId` is dropped from the stored value since it's now part of the key. On `setItem` failure (likely a quota issue), other entries with the prefix are removed and `setItem` is retried once; if it still fails, persistence is silently skipped and `location.reload()` remains the safety net on the read side. The fallback path also gets a small change: when restore legitimately fails (e.g., session storage cleared mid-session), `createDebugChannel` now returns `{ readable: new ReadableStream() }` rather than `{ readable: undefined }`. The empty stream never enqueues and never closes, so from the Flight client's perspective the debug channel is still active when the main stream closes — the unresolved chunk references no longer surface as a synchronous error before `location.reload()` tears the document down. The `bfcache-regression` test was updated to reproduce the bug. It now clicks an internal MPA link to `/target-page` instead of an external one, and asserts no console errors after the round trip via `assertNoConsoleErrors`. The fixture pre-warms `/target-page` compilation in parallel with the browser load in webpack mode, to avoid an unrelated Fast Refresh full-reload warning when the route is compiled on demand.
8addd31 to
ba5ac8d
Compare
timneutkens
approved these changes
May 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backports: