[Flight] Avoid unnecessary indirection when serializing debug info (#34797)

When a debug channel is hooked up, and we're serializing debug models,
if the result is an already outlined reference, we can emit it directly,
without also outlining the reference. This would create an unnecessary
indirection.

Before:

```
:N1760023808330.2688
0:D"$2"
0:D"$3"
0:D"$4"
0:"hi"

1:{"name":"Component","key":null,"env":"Server","stack":[],"props":{}}
2:{"time":3.0989999999999327}
3:"$1"
4:{"time":3.261792000000014}
```

After:

```
:N1760023786873.8916
0:D"$2"
0:D"$1"
0:D"$3"
0:"hi"

1:{"name":"Component","key":null,"env":"Server","stack":[],"props":{}}
2:{"time":2.4145829999999933}
3:{"time":2.5488749999999527}
```

Notice how the second debug info chunk is now directly referencing chunk
`1` in the debug channel, without outlining and referencing `"$1"` as
its own debug chunk `3`.

This not only simplifies the RSC payload, and reduces overhead. But more
importantly it helps the client resolve cyclic references when a model
has debug info that has a reference back to the model. The client is
currently not able to resolve such a cycle when those chunk indirections
are involved. Ideally, it would also be able to resolve them regardless,
but that requires more work. In the meantime, this fixes an immediate
issue.
This commit is contained in:
Hendrik Liebau
2025-10-10 21:44:28 +02:00
committed by GitHub
parent d44659744f
commit ead92181bd
2 changed files with 71 additions and 8 deletions

View File

@@ -3051,4 +3051,60 @@ describe('ReactFlightDOMBrowser', () => {
`);
}
});
it('should resolve a cycle between debug info and the value it produces when using a debug channel', async () => {
// Same as `should resolve a cycle between debug info and the value it produces`, but using a debug channel.
function Inner({style}) {
return <div style={style} />;
}
function Component({style}) {
return <Inner style={style} />;
}
const style = {};
const element = <Component style={style} />;
style.element = element;
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(element, webpackMap, {
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
}),
);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
debugChannel: {readable: debugReadableStream},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<ClientRoot response={response} />);
});
expect(container.innerHTML).toBe('<div></div>');
});
});

View File

@@ -4300,14 +4300,21 @@ function emitDebugChunk(
const json: string = serializeDebugModel(request, 500, debugInfo);
if (request.debugDestination !== null) {
// Outline the actual timing information to the debug channel.
const outlinedId = request.nextChunkId++;
const debugRow = outlinedId.toString(16) + ':' + json + '\n';
request.pendingDebugChunks++;
request.completedDebugChunks.push(stringToChunk(debugRow));
const row =
serializeRowHeader('D', id) + '"$' + outlinedId.toString(16) + '"\n';
request.completedRegularChunks.push(stringToChunk(row));
if (json[0] === '"' && json[1] === '$') {
// This is already an outlined reference so we can just emit it directly,
// without an unnecessary indirection.
const row = serializeRowHeader('D', id) + json + '\n';
request.completedRegularChunks.push(stringToChunk(row));
} else {
// Outline the debug information to the debug channel.
const outlinedId = request.nextChunkId++;
const debugRow = outlinedId.toString(16) + ':' + json + '\n';
request.pendingDebugChunks++;
request.completedDebugChunks.push(stringToChunk(debugRow));
const row =
serializeRowHeader('D', id) + '"$' + outlinedId.toString(16) + '"\n';
request.completedRegularChunks.push(stringToChunk(row));
}
} else {
const row = serializeRowHeader('D', id) + json + '\n';
request.completedRegularChunks.push(stringToChunk(row));