Skip to content

Commit f82e23e

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 f82e23e

1 file changed

Lines changed: 60 additions & 0 deletions

File tree

packages/react-server/src/ReactFlightServer.js

Lines changed: 60 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,62 @@ function emitImportChunk(
43264330
clientReferenceMetadata: ClientReferenceMetadata,
43274331
debug: boolean,
43284332
): void {
4333+
if (clientReferenceMetadata.length >= 3) {
4334+
// Dedupe individual chunk entries across client references.
4335+
const chunks = clientReferenceMetadata[2];
4336+
if (chunks.length > 0) {
4337+
const writtenChunkEntries =
4338+
__DEV__ && debug
4339+
? request.writtenDebugClientReferenceChunkEntries
4340+
: request.writtenClientReferenceChunkEntries;
4341+
const newChunks = [];
4342+
for (let i = 0; i < chunks.length; i++) {
4343+
const chunk = chunks[i];
4344+
// Only outline and dedupe string entries longer than 5 characters.
4345+
// Short strings (e.g. webpack chunk IDs) are kept inline since the
4346+
// reference would be close to the same length.
4347+
if (typeof chunk === 'string' && chunk.length > 5) {
4348+
let chunkId = writtenChunkEntries.get(chunk);
4349+
if (chunkId === undefined) {
4350+
chunkId = request.nextChunkId++;
4351+
// $FlowFixMe[incompatible-type] stringify can return null
4352+
const chunkJson: string = stringify(chunk);
4353+
const chunkRow = chunkId.toString(16) + ':' + chunkJson + '\n';
4354+
if (__DEV__ && debug) {
4355+
request.pendingDebugChunks++;
4356+
request.completedDebugChunks.push(stringToChunk(chunkRow));
4357+
} else {
4358+
request.pendingChunks++;
4359+
request.completedImportChunks.push(stringToChunk(chunkRow));
4360+
}
4361+
writtenChunkEntries.set(chunk, chunkId);
4362+
}
4363+
newChunks.push(serializeByValueID(chunkId));
4364+
} else {
4365+
// Other entries are emitted as-is. No bundler currently uses
4366+
// non-string chunk entries, but the opaque type allows for the
4367+
// possibility of other types in the future (e.g. tuples with SRI
4368+
// hashes). The dedupe strategy should be revisited then.
4369+
newChunks.push(chunk);
4370+
}
4371+
}
4372+
if (clientReferenceMetadata.length === 3) {
4373+
clientReferenceMetadata = [
4374+
clientReferenceMetadata[0],
4375+
clientReferenceMetadata[1],
4376+
newChunks,
4377+
];
4378+
} else {
4379+
clientReferenceMetadata = [
4380+
clientReferenceMetadata[0],
4381+
clientReferenceMetadata[1],
4382+
newChunks,
4383+
clientReferenceMetadata[3],
4384+
];
4385+
}
4386+
}
4387+
}
4388+
43294389
// $FlowFixMe[incompatible-type] stringify can return null
43304390
const json: string = stringify(clientReferenceMetadata);
43314391
const row = serializeRowHeader('I', id) + json + '\n';

0 commit comments

Comments
 (0)