Files
Hendrik LiebauandGitHub 24ae16139c Persist debug channel via IndexedDB without blocking hydration (#94243)
In development the debug channel that streams React's Server Component
debug information to the browser is buffered for the initial document
and persisted, so that it can be replayed when the browser later serves
the page from its HTTP cache — for example on back/forward navigation or
tab duplication — instead of forcing a full reload. This changes where
that buffer is persisted, moving it from `sessionStorage` to
`IndexedDB`.

The largest win comes from no longer having to serialize the data.
`sessionStorage` can only hold strings, so the binary debug chunks had
to be encoded into a string before being written and parsed back out
again on restore, which becomes expensive once the payload grows to
several megabytes. `IndexedDB` stores the chunks directly as the
`Uint8Array`s they already are, so there is no encode and parse round
trip on either side. Its asynchronous API helps a little on top of that,
since the write itself no longer has to happen synchronously.

The other improvement is that persistence no longer competes with
hydration. The initial document's debug stream closes while hydration is
still running, and the previous synchronous `sessionStorage` write
happened at exactly that moment, taking main thread time away from
hydration. The write is now deferred with `requestIdleCallback`, so it
only runs once the main thread is genuinely idle, which is after
hydration has drained, and it is skipped entirely if the page navigates
away before that happens, in which case a later restore simply falls
back to a reload. This is visible in the profiles below: with
`sessionStorage` there is still hydration work running after the
persistence task, whereas with the idle-scheduled `IndexedDB` write the
persistence only runs once hydration is done.

In a profile of a test page that deliberately transfers a large amount
of debug information, the persistence work on the main thread dropped
from more than 700 ms to roughly 25 to 45 ms. A real application with
far less debug data will see a smaller difference, so the test page
amplifies the effect, but the direction is the same.

The number of persisted entries stays bounded to 10, with the oldest
pruned on each write, and an end-to-end test covers the case where an
entry is pushed out by newer page loads so that navigating back to it
recovers through a page reload.

**Before with `sessionStorage`**:

<img width="973" height="627" alt="sessionStorage"
src="https://github.com/user-attachments/assets/b8ff8464-9ccf-4363-b1fe-78db0fd3e1eb"
/>

**After with idle-scheduled `IndexedDB`:**

<img width="973" height="627" alt="indexedDB"
src="https://github.com/user-attachments/assets/94a557a3-c82d-4771-bd81-3b4dfd906f25"
/>
2026-06-02 14:46:03 +02:00

29 lines
632 B
TypeScript

import { Suspense } from 'react'
import { ClientComponent } from './client'
// This page is only for manual performance profiling of the debug channel
// persistence (it streams a large amount of debug data). It is not used by any
// end-to-end test.
async function Home() {
for (let i = 0; i < 50; i++) {
await new Promise((resolve) =>
setTimeout(() => resolve('a'.repeat(1_000_000)))
)
}
return (
<div>
<h2>Large Debug Data</h2>
<ClientComponent />
</div>
)
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Home />
</Suspense>
)
}