Skip to content

Commit 0bf8555

Browse files
committed
[Flight] Deduplicate chunk entries in client reference metadata
Each client reference in the RSC stream includes an array of chunk entries (URLs or chunk ID/filename pairs) that the client needs to load. When many client components share the same chunks, these strings were repeated across every import row, significantly bloating the payload. This change deduplicates individual chunk entries across client references by outlining each unique entry as its own model row and replacing it with a `$` reference in the import metadata. The client's existing `parseModel` reviver resolves these references transparently, so no client-side changes are needed. Only string entries longer than 5 characters are outlined. Short strings like webpack chunk IDs are kept inline since the `$` reference would be close to the same size. If SRI (Subresource Integrity) support is added in the future, the dedup strategy for chunk entries will need to be revisited. The debug and production streams use separate dedup maps (`writtenDebugClientReferenceChunkEntries` and `writtenClientReferenceChunkEntries`) because chunk model rows must appear in the same stream as the import row that references them.
1 parent af24e98 commit 0bf8555

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

packages/react-server/src/ReactFlightServer.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ export type Request = {
580580
completedErrorChunks: Array<Chunk>,
581581
writtenSymbols: Map<symbol, number>,
582582
writtenClientReferences: Map<ClientReferenceKey, number>,
583+
writtenClientReferenceChunkEntries: Map<string, number>,
583584
writtenServerReferences: Map<ServerReference<any>, number>,
584585
writtenObjects: WeakMap<Reference, string>,
585586
temporaryReferences: void | TemporaryReferenceSet,
@@ -604,6 +605,7 @@ export type Request = {
604605
columnNumber: number,
605606
) => boolean,
606607
didWarnForKey: null | WeakSet<ReactComponentInfo>,
608+
writtenDebugClientReferenceChunkEntries: Map<string, number>,
607609
writtenDebugObjects: WeakMap<Reference, string>,
608610
deferredDebugObjects: null | DeferredDebugStore,
609611
};
@@ -699,6 +701,7 @@ function RequestInstance(
699701
this.completedErrorChunks = ([]: Array<Chunk>);
700702
this.writtenSymbols = new Map();
701703
this.writtenClientReferences = new Map();
704+
this.writtenClientReferenceChunkEntries = new Map();
702705
this.writtenServerReferences = new Map();
703706
this.writtenObjects = new WeakMap();
704707
this.temporaryReferences = temporaryReferences;
@@ -724,6 +727,7 @@ function RequestInstance(
724727
? defaultFilterStackFrame
725728
: filterStackFrame;
726729
this.didWarnForKey = null;
730+
this.writtenDebugClientReferenceChunkEntries = new Map();
727731
this.writtenDebugObjects = new WeakMap();
728732
this.deferredDebugObjects = keepDebugAlive
729733
? {
@@ -4326,6 +4330,64 @@ function emitImportChunk(
43264330
clientReferenceMetadata: ClientReferenceMetadata,
43274331
debug: boolean,
43284332
): void {
4333+
if (clientReferenceMetadata.length >= 3) {
4334+
// Dedupe individual chunk entries across client references.
4335+
// $FlowFixMe[invalid-tuple-index] guarded by length check above
4336+
const chunks = clientReferenceMetadata[2];
4337+
if (chunks.length > 0) {
4338+
const writtenChunkEntries =
4339+
__DEV__ && debug
4340+
? request.writtenDebugClientReferenceChunkEntries
4341+
: request.writtenClientReferenceChunkEntries;
4342+
const newChunks = [];
4343+
for (let i = 0; i < chunks.length; i++) {
4344+
const chunk = chunks[i];
4345+
// Only outline and dedupe string entries longer than 5 characters.
4346+
// Short strings (e.g. webpack chunk IDs) are kept inline since the
4347+
// reference would be close to the same length.
4348+
if (typeof chunk === 'string' && chunk.length > 5) {
4349+
let chunkId = writtenChunkEntries.get(chunk);
4350+
if (chunkId === undefined) {
4351+
chunkId = request.nextChunkId++;
4352+
// $FlowFixMe[incompatible-type] stringify can return null
4353+
const chunkJson: string = stringify(chunk);
4354+
const chunkRow = chunkId.toString(16) + ':' + chunkJson + '\n';
4355+
if (__DEV__ && debug) {
4356+
request.pendingDebugChunks++;
4357+
request.completedDebugChunks.push(stringToChunk(chunkRow));
4358+
} else {
4359+
request.pendingChunks++;
4360+
request.completedImportChunks.push(stringToChunk(chunkRow));
4361+
}
4362+
writtenChunkEntries.set(chunk, chunkId);
4363+
}
4364+
newChunks.push(serializeByValueID(chunkId));
4365+
} else {
4366+
// Other entries are emitted as-is. No bundler currently uses
4367+
// non-string chunk entries, but the opaque type allows for the
4368+
// possibility of other types in the future (e.g. tuples with SRI
4369+
// hashes). The dedupe strategy should be revisited then.
4370+
newChunks.push(chunk);
4371+
}
4372+
}
4373+
if (clientReferenceMetadata.length === 3) {
4374+
clientReferenceMetadata = [
4375+
clientReferenceMetadata[0],
4376+
clientReferenceMetadata[1],
4377+
newChunks,
4378+
];
4379+
} else {
4380+
clientReferenceMetadata = [
4381+
clientReferenceMetadata[0],
4382+
clientReferenceMetadata[1],
4383+
newChunks,
4384+
// $FlowFixMe[invalid-tuple-index] guarded by length check above
4385+
clientReferenceMetadata[3],
4386+
];
4387+
}
4388+
}
4389+
}
4390+
43294391
// $FlowFixMe[incompatible-type] stringify can return null
43304392
const json: string = stringify(clientReferenceMetadata);
43314393
const row = serializeRowHeader('I', id) + json + '\n';

0 commit comments

Comments
 (0)