Fix dev mode hydration failure when page is served from HTTP cache#92892
Merged
unstubbable merged 1 commit intocanaryfrom Apr 16, 2026
Merged
Fix dev mode hydration failure when page is served from HTTP cache#92892unstubbable merged 1 commit intocanaryfrom
unstubbable merged 1 commit intocanaryfrom
Conversation
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.
Contributor
Tests Passed |
gnoff
approved these changes
Apr 16, 2026
This was referenced Apr 23, 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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
PR #88182 introduced an experimental option to use
Cache-Control: no-cacheinstead ofno-storein dev mode, and PR #91503 made it the default. Withno-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
hydrateRootis 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
TransformStreamingetOrCreateDebugChannelReadableWriterPair. Once all chunks have been received, the buffer is eagerly persisted tosessionStorage. When the page detects it was served from HTTP cache (viaPerformanceNavigationTiming.transferSize === 0),createDebugChannelrestores the debug data fromsessionStorageand replays it as a syntheticReadableStreaminstead 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 tolocation.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