Skip to content

Commit 3025aa3

Browse files
authored
[Flight] Don't serialize toJSON in Debug path and omit wide arrays (facebook#34759)
There's a couple of issues with serializing Buffer in the debug renders. For one, the Node.js Buffer has a `toJSON` on it which turns the binary data into a JSON array which is very inefficient to serialize compared to the real buffer. For debug info we never really want to resolve these and unlike the regular render we can't error. So this uses the trick where we read the original value. It's still unfortunate that this intermediate gets created at all but at least now we're not serializing it. Second, we have a limit on depth of objects but we didn't have a limit on width like large arrays or typed arrays. This omits large arrays from the payload when possible and make them deferred when there's a debug channel.
1 parent a4eb2df commit 3025aa3

File tree

3 files changed

+529
-291
lines changed

3 files changed

+529
-291
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,49 @@ export function resolveRequest(): null | Request {
849849
return null;
850850
}
851851

852+
function isTypedArray(value: any): boolean {
853+
if (value instanceof ArrayBuffer) {
854+
return true;
855+
}
856+
if (value instanceof Int8Array) {
857+
return true;
858+
}
859+
if (value instanceof Uint8Array) {
860+
return true;
861+
}
862+
if (value instanceof Uint8ClampedArray) {
863+
return true;
864+
}
865+
if (value instanceof Int16Array) {
866+
return true;
867+
}
868+
if (value instanceof Uint16Array) {
869+
return true;
870+
}
871+
if (value instanceof Int32Array) {
872+
return true;
873+
}
874+
if (value instanceof Uint32Array) {
875+
return true;
876+
}
877+
if (value instanceof Float32Array) {
878+
return true;
879+
}
880+
if (value instanceof Float64Array) {
881+
return true;
882+
}
883+
if (value instanceof BigInt64Array) {
884+
return true;
885+
}
886+
if (value instanceof BigUint64Array) {
887+
return true;
888+
}
889+
if (value instanceof DataView) {
890+
return true;
891+
}
892+
return false;
893+
}
894+
852895
function serializeDebugThenable(
853896
request: Request,
854897
counter: {objectLimit: number},
@@ -906,6 +949,17 @@ function serializeDebugThenable(
906949
enqueueFlush(request);
907950
return;
908951
}
952+
if (
953+
(isArray(value) && value.length > 200) ||
954+
(isTypedArray(value) && value.byteLength > 1000)
955+
) {
956+
// If this should be deferred, but we don't have a debug channel installed
957+
// it would get omitted. We can't omit outlined models but we can avoid
958+
// resolving the Promise at all by halting it.
959+
emitDebugHaltChunk(request, id);
960+
enqueueFlush(request);
961+
return;
962+
}
909963
emitOutlinedDebugModelChunk(request, id, counter, value);
910964
enqueueFlush(request);
911965
},
@@ -3066,6 +3120,10 @@ function serializeDebugTypedArray(
30663120
tag: string,
30673121
typedArray: $ArrayBufferView,
30683122
): string {
3123+
if (typedArray.byteLength > 1000 && !doNotLimit.has(typedArray)) {
3124+
// Defer large typed arrays.
3125+
return serializeDeferredObject(request, typedArray);
3126+
}
30693127
request.pendingDebugChunks++;
30703128
const bufferId = request.nextChunkId++;
30713129
emitTypedArrayChunk(request, bufferId, tag, typedArray, true);
@@ -4820,9 +4878,17 @@ function renderDebugModel(
48204878
}
48214879

48224880
if (isArray(value)) {
4881+
if (value.length > 200 && !doNotLimit.has(value)) {
4882+
// Defer large arrays. They're heavy to serialize.
4883+
// TODO: Consider doing the same for objects with many properties too.
4884+
return serializeDeferredObject(request, value);
4885+
}
48234886
return value;
48244887
}
48254888

4889+
if (value instanceof Date) {
4890+
return serializeDate(value);
4891+
}
48264892
if (value instanceof Map) {
48274893
return serializeDebugMap(request, counter, value);
48284894
}
@@ -4930,15 +4996,6 @@ function renderDebugModel(
49304996
}
49314997

49324998
if (typeof value === 'string') {
4933-
if (value[value.length - 1] === 'Z') {
4934-
// Possibly a Date, whose toJSON automatically calls toISOString
4935-
// Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
4936-
// $FlowFixMe[incompatible-use]
4937-
const originalValue = parent[parentPropertyName];
4938-
if (originalValue instanceof Date) {
4939-
return serializeDateFromDateJSON(value);
4940-
}
4941-
}
49424999
if (value.length >= 1024) {
49435000
// Large strings are counted towards the object limit.
49445001
if (counter.objectLimit <= 0) {
@@ -5036,10 +5093,6 @@ function renderDebugModel(
50365093
return serializeBigInt(value);
50375094
}
50385095

5039-
if (value instanceof Date) {
5040-
return serializeDate(value);
5041-
}
5042-
50435096
return 'unknown type ' + typeof value;
50445097
}
50455098

@@ -5058,12 +5111,15 @@ function serializeDebugModel(
50585111
value: ReactClientValue,
50595112
): ReactJSONValue {
50605113
try {
5114+
// By-pass toJSON and use the original value.
5115+
// $FlowFixMe[incompatible-use]
5116+
const originalValue = this[parentPropertyName];
50615117
return renderDebugModel(
50625118
request,
50635119
counter,
50645120
this,
50655121
parentPropertyName,
5066-
value,
5122+
originalValue,
50675123
);
50685124
} catch (x) {
50695125
return (
@@ -5114,12 +5170,15 @@ function emitOutlinedDebugModelChunk(
51145170
value: ReactClientValue,
51155171
): ReactJSONValue {
51165172
try {
5173+
// By-pass toJSON and use the original value.
5174+
// $FlowFixMe[incompatible-use]
5175+
const originalValue = this[parentPropertyName];
51175176
return renderDebugModel(
51185177
request,
51195178
counter,
51205179
this,
51215180
parentPropertyName,
5122-
value,
5181+
originalValue,
51235182
);
51245183
} catch (x) {
51255184
return (

0 commit comments

Comments
 (0)