Skip to content

Commit df81cb6

Browse files
committed
Optimize large strings
1 parent 194d544 commit df81cb6

File tree

11 files changed

+148
-11
lines changed

11 files changed

+148
-11
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ function createResolvedModuleChunk<T>(
288288
return new Chunk(RESOLVED_MODULE, value, null, response);
289289
}
290290

291+
function createInitializedTextChunk(
292+
response: Response,
293+
value: string,
294+
): InitializedChunk<string> {
295+
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
296+
return new Chunk(INITIALIZED, value, null, response);
297+
}
298+
291299
function resolveModelChunk<T>(
292300
chunk: SomeChunk<T>,
293301
value: UninitializedModel,
@@ -704,6 +712,13 @@ function resolveModel(
704712
}
705713
}
706714

715+
function resolveText(response: Response, id: number, text: string): void {
716+
const chunks = response._chunks;
717+
// We assume that we always reference large strings after they've been
718+
// emitted.
719+
chunks.set(id, createInitializedTextChunk(response, text));
720+
}
721+
707722
function resolveModule(
708723
response: Response,
709724
id: number,
@@ -818,7 +833,7 @@ function resolveHint(
818833
code: string,
819834
model: UninitializedModel,
820835
): void {
821-
const hintModel = parseModel<HintModel>(response, model);
836+
const hintModel: HintModel = parseModel(response, model);
822837
dispatchHint(code, hintModel);
823838
}
824839

@@ -869,6 +884,10 @@ function processFullRow(
869884
}
870885
return;
871886
}
887+
case 84 /* "T" */: {
888+
resolveText(response, id, row);
889+
return;
890+
}
872891
default: {
873892
// We assume anything else is JSON.
874893
resolveModel(response, id, row);
@@ -898,17 +917,30 @@ export function processBinaryChunk(
898917
}
899918
case ROW_TAG: {
900919
const resolvedRowTag = chunk[i];
901-
if (resolvedRowTag > 64 && resolvedRowTag < 91) {
920+
if (resolvedRowTag === 84 /* "T" */) {
921+
response._rowTag = resolvedRowTag;
922+
response._rowState = ROW_LENGTH;
923+
i++;
924+
} else if (resolvedRowTag > 64 && resolvedRowTag < 91 /* "A"-"Z" */) {
902925
response._rowTag = resolvedRowTag;
926+
response._rowState = ROW_CHUNK_BY_NEWLINE;
903927
i++;
904928
} else {
929+
response._rowTag = 0;
930+
response._rowState = ROW_CHUNK_BY_NEWLINE;
905931
// This was an unknown tag so it was probably part of the data.
906932
}
907-
response._rowState = ROW_CHUNK_BY_NEWLINE;
908933
continue;
909934
}
910935
case ROW_LENGTH: {
911-
// TODO
936+
const byte = chunk[i++];
937+
if (byte === 44 /* "," */) {
938+
// Finished the rowLength, next we'll buffer up to that length.
939+
response._rowState = ROW_CHUNK_BY_LENGTH;
940+
} else {
941+
response._rowLength =
942+
(response._rowLength << 4) | (byte > 96 ? byte - 87 : byte - 48);
943+
}
912944
continue;
913945
}
914946
case ROW_CHUNK_BY_NEWLINE: {

packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export function clonePrecomputedChunk(
5757
return chunk;
5858
}
5959

60+
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
61+
throw new Error('Not implemented.');
62+
}
63+
6064
export function closeWithError(destination: Destination, error: mixed): void {
6165
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
6266
destination.destroy(error);

packages/react-server-dom-fb/src/ReactServerStreamConfigFB.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export function clonePrecomputedChunk(
6363
return chunk;
6464
}
6565

66+
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
67+
throw new Error('Not implemented.');
68+
}
69+
6670
export function closeWithError(destination: Destination, error: mixed): void {
6771
destination.done = true;
6872
destination.fatal = true;

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,25 @@ describe('ReactFlightDOMEdge', () => {
9898
const result = await readResult(ssrStream);
9999
expect(result).toEqual('<span>Client Component</span>');
100100
});
101+
102+
it('should encode long string in a compact format', async () => {
103+
const testString = '"\n\t'.repeat(500) + '🙃';
104+
105+
const stream = ReactServerDOMServer.renderToReadableStream({
106+
text: testString,
107+
});
108+
const [stream1, stream2] = stream.tee();
109+
110+
const serializedContent = await readResult(stream1);
111+
// The content should be compact an unescaped
112+
expect(serializedContent.length).toBeLessThan(2000);
113+
expect(serializedContent).not.toContain('\\n');
114+
expect(serializedContent).not.toContain('\\t');
115+
expect(serializedContent).not.toContain('\\"');
116+
expect(serializedContent).toContain('\t');
117+
118+
const result = await ReactServerDOMClient.createFromReadableStream(stream2);
119+
// Should still match the result when parsed
120+
expect(result.text).toBe(testString);
121+
});
101122
});

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,31 @@ describe('ReactFlightDOMNode', () => {
104104
const result = await readResult(ssrStream);
105105
expect(result).toEqual('<span>Client Component</span>');
106106
});
107+
108+
it('should encode long string in a compact format', async () => {
109+
const testString = '"\n\t'.repeat(500) + '🙃';
110+
111+
const stream = ReactServerDOMServer.renderToPipeableStream({
112+
text: testString,
113+
});
114+
115+
const readable = new Stream.PassThrough();
116+
117+
const stringResult = readResult(readable);
118+
const parsedResult = ReactServerDOMClient.createFromNodeStream(readable);
119+
120+
stream.pipe(readable);
121+
122+
const serializedContent = await stringResult;
123+
// The content should be compact an unescaped
124+
expect(serializedContent.length).toBeLessThan(2000);
125+
expect(serializedContent).not.toContain('\\n');
126+
expect(serializedContent).not.toContain('\\t');
127+
expect(serializedContent).not.toContain('\\"');
128+
expect(serializedContent).toContain('\t');
129+
130+
const result = await parsedResult;
131+
// Should still match the result when parsed
132+
expect(result.text).toBe(testString);
133+
});
107134
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
beginWriting,
1616
writeChunkAndReturn,
1717
stringToChunk,
18+
byteLengthOfChunk,
1819
completeWriting,
1920
close,
2021
closeWithError,
@@ -171,7 +172,7 @@ export type Request = {
171172
pingedTasks: Array<Task>,
172173
completedImportChunks: Array<Chunk>,
173174
completedHintChunks: Array<Chunk>,
174-
completedJSONChunks: Array<Chunk>,
175+
completedRegularChunks: Array<Chunk>,
175176
completedErrorChunks: Array<Chunk>,
176177
writtenSymbols: Map<symbol, number>,
177178
writtenClientReferences: Map<ClientReferenceKey, number>,
@@ -230,7 +231,7 @@ export function createRequest(
230231
pingedTasks: pingedTasks,
231232
completedImportChunks: ([]: Array<Chunk>),
232233
completedHintChunks: ([]: Array<Chunk>),
233-
completedJSONChunks: ([]: Array<Chunk>),
234+
completedRegularChunks: ([]: Array<Chunk>),
234235
completedErrorChunks: ([]: Array<Chunk>),
235236
writtenSymbols: new Map(),
236237
writtenClientReferences: new Map(),
@@ -715,11 +716,25 @@ function serializeServerReference(
715716
metadataId,
716717
serverReferenceMetadata,
717718
);
718-
request.completedJSONChunks.push(processedChunk);
719+
request.completedRegularChunks.push(processedChunk);
719720
writtenServerReferences.set(serverReference, metadataId);
720721
return serializeServerReferenceID(metadataId);
721722
}
722723

724+
function serializeLargeTextString(request: Request, text: string): string {
725+
request.pendingChunks += 2;
726+
const textId = request.nextChunkId++;
727+
const textChunk = stringToChunk(text);
728+
const headerChunk = processTextHeader(
729+
request,
730+
textId,
731+
text,
732+
byteLengthOfChunk(textChunk),
733+
);
734+
request.completedRegularChunks.push(headerChunk, textChunk);
735+
return serializeByValueID(textId);
736+
}
737+
723738
function escapeStringValue(value: string): string {
724739
if (value[0] === '$') {
725740
// We need to escape $ prefixed strings since we use those to encode
@@ -960,7 +975,12 @@ function resolveModelToJSON(
960975
return serializeDateFromDateJSON(value);
961976
}
962977
}
963-
978+
if (value.length > 1024) {
979+
// For large strings, we encode them outside the JSON payload so that we
980+
// don't have to double encode and double parse the strings. This can also
981+
// be more compact in case the string has a lot of escaped characters.
982+
return serializeLargeTextString(request, value);
983+
}
964984
return escapeStringValue(value);
965985
}
966986

@@ -1152,7 +1172,7 @@ function emitProviderChunk(
11521172
): void {
11531173
const contextReference = serializeProviderReference(contextName);
11541174
const processedChunk = processReferenceChunk(request, id, contextReference);
1155-
request.completedJSONChunks.push(processedChunk);
1175+
request.completedRegularChunks.push(processedChunk);
11561176
}
11571177

11581178
function retryTask(request: Request, task: Task): void {
@@ -1216,7 +1236,7 @@ function retryTask(request: Request, task: Task): void {
12161236
}
12171237

12181238
const processedChunk = processModelChunk(request, task.id, value);
1219-
request.completedJSONChunks.push(processedChunk);
1239+
request.completedRegularChunks.push(processedChunk);
12201240
request.abortableTasks.delete(task);
12211241
task.status = COMPLETED;
12221242
} catch (thrownValue) {
@@ -1323,7 +1343,7 @@ function flushCompletedChunks(
13231343
hintChunks.splice(0, i);
13241344

13251345
// Next comes model data.
1326-
const jsonChunks = request.completedJSONChunks;
1346+
const jsonChunks = request.completedRegularChunks;
13271347
i = 0;
13281348
for (; i < jsonChunks.length; i++) {
13291349
request.pendingChunks--;
@@ -1545,3 +1565,13 @@ function processHintChunk(
15451565
const row = serializeRowHeader('H' + code, id) + json + '\n';
15461566
return stringToChunk(row);
15471567
}
1568+
1569+
function processTextHeader(
1570+
request: Request,
1571+
id: number,
1572+
text: string,
1573+
binaryLength: number,
1574+
): Chunk {
1575+
const row = id.toString(16) + ':T' + binaryLength.toString(16) + ',';
1576+
return stringToChunk(row);
1577+
}

packages/react-server/src/ReactServerStreamConfigBrowser.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ export function clonePrecomputedChunk(
139139
: precomputedChunk;
140140
}
141141

142+
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
143+
return chunk.byteLength;
144+
}
145+
142146
export function closeWithError(destination: Destination, error: mixed): void {
143147
// $FlowFixMe[method-unbinding]
144148
if (typeof destination.error === 'function') {

packages/react-server/src/ReactServerStreamConfigBun.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export function clonePrecomputedChunk(
6666
return chunk;
6767
}
6868

69+
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
70+
throw new Error('Not implemented.');
71+
}
72+
6973
export function closeWithError(destination: Destination, error: mixed): void {
7074
if (typeof destination.error === 'function') {
7175
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.

packages/react-server/src/ReactServerStreamConfigEdge.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ export function clonePrecomputedChunk(
139139
: precomputedChunk;
140140
}
141141

142+
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
143+
return chunk.byteLength;
144+
}
145+
142146
export function closeWithError(destination: Destination, error: mixed): void {
143147
// $FlowFixMe[method-unbinding]
144148
if (typeof destination.error === 'function') {

packages/react-server/src/ReactServerStreamConfigNode.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ export function clonePrecomputedChunk(
215215
: precomputedChunk;
216216
}
217217

218+
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
219+
return typeof chunk === 'string'
220+
? Buffer.byteLength(chunk, 'utf8')
221+
: chunk.byteLength;
222+
}
223+
218224
export function closeWithError(destination: Destination, error: mixed): void {
219225
// $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types.
220226
destination.destroy(error);

0 commit comments

Comments
 (0)