This note summarizes React's RSC debug channel and a possible minimal example for @vitejs/plugin-rsc. It is based on these revisions:
| Project | Revision |
|---|---|
| React | b740af25 |
| Next.js | 153bf8ac |
| Waku | 3f88539d |
React includes component ownership, stacks, console entries, async metadata, and timing as debug data in development RSC streams. A debug channel moves this data from the normal RSC response into a separate Flight stream. The browser passes both streams to React's client decoder, which reconstructs the same debug information.
A duplex channel additionally lets React retain omitted or large values and return them later when the browser requests them. This is useful for richer inspection without eagerly increasing the main RSC payload.
Performance tracks are related but independent. Inline Flight debug rows can produce Server Components tracks without a debug channel. The channel changes how debug data is transported; it is not required to enable performance tracks.
React defines the stream protocol but not its network transport. A framework must correlate every RSC render with the correct browser-side debug stream and carry the bytes over HMR, WebSocket, or another persistent connection.
React accepts a development-only debugChannel option shaped like this:
interface DebugChannel {
readable?: ReadableStream<Uint8Array>
writable?: WritableStream<Uint8Array>
}The direction is relative to React:
- On the RSC server,
writablereceives the debug Flight stream andreadablereceives commands from the browser. See React's server entry. - In the browser,
readablesupplies the debug Flight stream andwritablesends commands to the server. See React's browser entry.
The channel is not an observability JSON protocol. Server-to-browser chunks are ordinary Flight rows and may include binary chunks. A transport should treat them as opaque bytes.
@vitejs/plugin-rsc already exposes and forwards React's option on the server and in the browser. An application or framework still needs to implement correlation, transport, buffering, bootstrap ID propagation, and cleanup.
Without a debug destination, React emits development debug models in the main Flight stream. With a debug destination, React writes the large model to the debug stream and leaves a reference in a D row on the main stream. See emitDebugChunk.
The offloaded data includes component ownership and stacks, environment metadata, async and I/O information, replayable server console entries, and performance timing rows. Normal application model rows, client-reference imports, hints, and application errors remain on the main RSC stream.
When the reverse channel is available, React can retain deferred debug objects. The browser can request an object with Q:<hex-id>, request a promise with P:<hex-id>, release IDs with R:<ids>, or close the channel with an empty message. See the server command handler and client command writer.
Once rendering starts with a debug destination, delivery is part of response correctness rather than optional telemetry. The main stream can refer to data expected on the debug stream, so losing or mismatching that stream can block decoding.
Waku implements a duplex channel over Vite's existing HMR connection.
- Dev-server middleware creates or accepts a debug ID and registers paired Web streams before request handling. See
rsc-devtools.ts. - The RSC handler consumes the pair by ID and passes it to React. See
handler.tsandrender.ts. - Server output uses a
waku:debug-dataHMR event, while browser commands usewaku:debug-cmd. Chunks are base64 encoded because custom HMR event payloads are JSON-shaped. Seereact-debug-channel.ts. - The server buffers output until the browser announces readiness. This handles initial HTML, where rendering may finish before the browser establishes HMR. See the session logic.
- Initial SSR embeds the debug ID beside the inlined RSC response. Later client fetches generate an ID first and send it in a private header. See
initial-rsc.tsand the client integration.
Waku demonstrates the complete protocol, including lazy browser-to-server requests and release commands.
Next.js implements a one-way server-to-browser channel over its HMR WebSocket.
- Rendering creates a stream pair and passes only the writable side to React. The readable side is registered with the dev hot reloader. See the Node implementation and Web implementation.
- Debug bytes are batched into
REACT_DEBUG_CHUNKHMR messages. A null chunk marks the end of the stream. Seedebug-channel.ts. - The browser routes messages by request ID into a
TransformStream. See the browser implementation. - Initial HTML embeds its request ID, while later RSC requests send both the owning document ID and a new request ID. See
fetch-server-response.ts.
Next.js retains streams for repeated decoding and HTTP-cache restoration. That framework-specific lifecycle machinery is not inherent to a minimal debug-channel transport. Its browser-to-server return channel is not currently integrated.
- React owns serialization, decoding, lazy object commands, and response lifetime. Frameworks should transport opaque bytes rather than parse the channel.
- Correlation and reliable delivery are correctness requirements because main-stream rows can reference debug-stream values.
- Initial HTML is the difficult lifecycle case because debug output can exist before the browser connection. Both Waku and Next.js buffer or retain it until the client connects.
- Later RSC requests are simpler because the connected browser can allocate an ID before fetching and include it in a private request header.
- Duplex transport enables React's lazy
Q,P, andRcommands. Waku supports it, while Next.js currently implements only server-to-browser delivery. - Session count, buffered bytes, timeouts, cancellation, and disconnect cleanup need explicit bounds. Requests from browsers without JavaScript may never attach to their channels.
- A fallback decision must happen before rendering. Once React renders with a debug destination, silently dropping the debug stream is unsafe.
A focused @vitejs/plugin-rsc example can use Vite HMR as a transport scoped to the example. It should demonstrate React's debug-channel feature rather than present it as required performance-track plumbing.
browser createDebugChannel(id)
readable <- Vite HMR "rsc:debug:data" <- server pair.writable <- React
writable -> Vite HMR "rsc:debug:cmd" -> server pair.readable -> React
RSC fetch header: X-Vite-RSC-Debug-Id: <uuid>
initial HTML bootstrap: self.__VITE_RSC_DEBUG_ID__ = <uuid>
server registry: Map<debugId, { channel, buffered output, ready/ended state }>
The minimum representative flow is:
- In development, middleware assigns a UUID before the request handler runs. Browser-created RSC requests supply a UUID in a private header; initial HTML requests receive a generated UUID.
- Register paired streams before rendering and buffer server output until the matching browser announces readiness.
- Pass the server pair to
renderToReadableStream. - Pass the initial ID through SSR and prepend a small bootstrap assignment beside the browser bootstrap script. Do not inline the debug bytes into HTML.
- In the browser entry, create the initial channel from the embedded ID and pass it to
createFromReadableStream. - For navigation and HMR refreshes, create a UUID before
fetch, attach it to the request, and pass the matching channel tocreateFromFetch. - Close and delete sessions on completion, cancellation, HMR disconnect, and timeout. Bound buffered bytes and active session count.
Useful validation includes initial output-before-ready, ready-before-request, subsequent fetch correlation, completion, cancellation, concurrent request IDs, and a production build where the development-only feature is absent.