@samitouri / QOS-React / commits / c80c69fa96

[Flight] Remove back pointers to the Response from the Chunks (#33620)

This frees some memory that will be even more important in a follow up. Currently, all `ReactPromise` instances hold onto their original `Response`. The `Response` holds onto all objects that were in that response since they're needed in case the parsed content ends up referring to an existing object. If everything you retain are plain objects then that's fine and the `Response` gets GC:ed, but if you're retaining a `Promise` itself then it holds onto the whole `Response`. The only thing that needs this reference at all is a `ResolvedModelChunk` since it will lazily initialize e.g. by calling `.then` on itself and so we need to know where to find any sibling chunks it may refer to. However, we can just store the `Response` on the `reason` field for this particular state. That way when all lazy values are touched and initialized the `Response` is freed. We also free up some memory by getting rid of the extra field.

Sebastian Markbåge committed Jun 23, 2025 at 18:37 UTC c80c69fa965a517c2978b0cb39bdb49c0e1cb5c8
1 file changed +34 -39
packages/react-client/src/ReactFlightClient.js
+34 -39
@@ -165,7 +165,6 @@ type PendingChunk<T> = {
165 status: 'pending',
166 value: null | Array<(T) => mixed>,
167 reason: null | Array<(mixed) => mixed>,
168 - _response: Response,
168 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
169 _debugInfo?: null | ReactDebugInfo, // DEV-only
170 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -174,7 +173,6 @@ type BlockedChunk<T> = {
173 status: 'blocked',
174 value: null | Array<(T) => mixed>,
175 reason: null | Array<(mixed) => mixed>,
177 - _response: Response,
176 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
177 _debugInfo?: null | ReactDebugInfo, // DEV-only
178 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -182,8 +180,7 @@ type BlockedChunk<T> = {
180 type ResolvedModelChunk<T> = {
181 status: 'resolved_model',
182 value: UninitializedModel,
185 - reason: null,
186 - _response: Response,
183 + reason: Response,
184 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
185 _debugInfo?: null | ReactDebugInfo, // DEV-only
186 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -192,7 +189,6 @@ type ResolvedModuleChunk<T> = {
189 status: 'resolved_module',
190 value: ClientReference<T>,
191 reason: null,
195 - _response: Response,
192 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
193 _debugInfo?: null | ReactDebugInfo, // DEV-only
194 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -201,7 +197,6 @@ type InitializedChunk<T> = {
197 status: 'fulfilled',
198 value: T,
199 reason: null | FlightStreamController,
204 - _response: Response,
200 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
201 _debugInfo?: null | ReactDebugInfo, // DEV-only
202 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -212,7 +207,6 @@ type InitializedStreamChunk<
207 status: 'fulfilled',
208 value: T,
209 reason: FlightStreamController,
215 - _response: Response,
210 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
211 _debugInfo?: null | ReactDebugInfo, // DEV-only
212 then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
@@ -221,7 +215,6 @@ type ErroredChunk<T> = {
215 status: 'rejected',
216 value: null,
217 reason: mixed,
224 - _response: Response,
218 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
219 _debugInfo?: null | ReactDebugInfo, // DEV-only
220 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -230,7 +223,6 @@ type HaltedChunk<T> = {
223 status: 'halted',
224 value: null,
225 reason: null,
233 - _response: Response,
226 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
227 _debugInfo?: null | ReactDebugInfo, // DEV-only
228 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -245,16 +237,10 @@ type SomeChunk<T> =
237 | HaltedChunk<T>;
238
239 // $FlowFixMe[missing-this-annot]
248 -function ReactPromise(
249 - status: any,
250 - value: any,
251 - reason: any,
252 - response: Response,
253 -) {
240 +function ReactPromise(status: any, value: any, reason: any) {
241 this.status = status;
242 this.value = value;
243 this.reason = reason;
257 - this._response = response;
244 if (enableProfilerTimer && enableComponentPerformanceTrack) {
245 this._children = [];
246 }
@@ -401,12 +387,12 @@ export function getRoot<T>(response: Response): Thenable<T> {
387
388 function createPendingChunk<T>(response: Response): PendingChunk<T> {
389 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
404 - return new ReactPromise(PENDING, null, null, response);
390 + return new ReactPromise(PENDING, null, null);
391 }
392
393 function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
394 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
409 - return new ReactPromise(BLOCKED, null, null, response);
395 + return new ReactPromise(BLOCKED, null, null);
396 }
397
398 function createErrorChunk<T>(
@@ -414,7 +400,7 @@ function createErrorChunk<T>(
400 error: mixed,
401 ): ErroredChunk<T> {
402 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
417 - return new ReactPromise(ERRORED, null, error, response);
403 + return new ReactPromise(ERRORED, null, error);
404 }
405
406 function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
@@ -486,7 +472,7 @@ function createResolvedModelChunk<T>(
472 value: UninitializedModel,
473 ): ResolvedModelChunk<T> {
474 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
489 - return new ReactPromise(RESOLVED_MODEL, value, null, response);
475 + return new ReactPromise(RESOLVED_MODEL, value, response);
476 }
477
478 function createResolvedModuleChunk<T>(
@@ -494,7 +480,7 @@ function createResolvedModuleChunk<T>(
480 value: ClientReference<T>,
481 ): ResolvedModuleChunk<T> {
482 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
497 - return new ReactPromise(RESOLVED_MODULE, value, null, response);
483 + return new ReactPromise(RESOLVED_MODULE, value, null);
484 }
485
486 function createInitializedTextChunk(
@@ -502,7 +488,7 @@ function createInitializedTextChunk(
488 value: string,
489 ): InitializedChunk<string> {
490 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
505 - return new ReactPromise(INITIALIZED, value, null, response);
491 + return new ReactPromise(INITIALIZED, value, null);
492 }
493
494 function createInitializedBufferChunk(
@@ -510,7 +496,7 @@ function createInitializedBufferChunk(
496 value: $ArrayBufferView | ArrayBuffer,
497 ): InitializedChunk<Uint8Array> {
498 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
513 - return new ReactPromise(INITIALIZED, value, null, response);
499 + return new ReactPromise(INITIALIZED, value, null);
500 }
501
502 function createInitializedIteratorResultChunk<T>(
@@ -519,12 +505,7 @@ function createInitializedIteratorResultChunk<T>(
505 done: boolean,
506 ): InitializedChunk<IteratorResult<T, T>> {
507 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
522 - return new ReactPromise(
523 - INITIALIZED,
524 - {done: done, value: value},
525 - null,
526 - response,
527 - );
508 + return new ReactPromise(INITIALIZED, {done: done, value: value}, null);
509 }
510
511 function createInitializedStreamChunk<
@@ -537,7 +518,7 @@ function createInitializedStreamChunk<
518 // We use the reason field to stash the controller since we already have that
519 // field. It's a bit of a hack but efficient.
520 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
540 - return new ReactPromise(INITIALIZED, value, controller, response);
521 + return new ReactPromise(INITIALIZED, value, controller);
522 }
523
524 function createResolvedIteratorResultChunk<T>(
@@ -549,10 +530,11 @@ function createResolvedIteratorResultChunk<T>(
530 const iteratorResultJSON =
531 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
532 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
552 - return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response);
533 + return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response);
534 }
535
536 function resolveIteratorResultChunk<T>(
537 + response: Response,
538 chunk: SomeChunk<IteratorResult<T, T>>,
539 value: UninitializedModel,
540 done: boolean,
@@ -560,10 +542,11 @@ function resolveIteratorResultChunk<T>(
542 // To reuse code as much code as possible we add the wrapper element as part of the JSON.
543 const iteratorResultJSON =
544 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
563 - resolveModelChunk(chunk, iteratorResultJSON);
545 + resolveModelChunk(response, chunk, iteratorResultJSON);
546 }
547
548 function resolveModelChunk<T>(
549 + response: Response,
550 chunk: SomeChunk<T>,
551 value: UninitializedModel,
552 ): void {
@@ -580,6 +563,7 @@ function resolveModelChunk<T>(
563 const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
564 resolvedChunk.status = RESOLVED_MODEL;
565 resolvedChunk.value = value;
566 + resolvedChunk.reason = response;
567 if (resolveListeners !== null) {
568 // This is unfortunate that we're reading this eagerly if
569 // we already have listeners attached since they might no
@@ -625,6 +609,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
609 initializingHandler = null;
610
611 const resolvedModel = chunk.value;
612 + const response = chunk.reason;
613
614 // We go to the BLOCKED state until we've fully resolved this.
615 // We do this before parsing in case we try to initialize the same chunk
@@ -639,7 +624,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
624 }
625
626 try {
642 - const value: T = parseModel(chunk._response, resolvedModel);
627 + const value: T = parseModel(response, resolvedModel);
628 // Invoke any listeners added while resolving this model. I.e. cyclic
629 // references. This may or may not fully resolve the model depending on
630 // if they were blocked.
@@ -1862,7 +1847,7 @@ function resolveModel(
1847 if (!chunk) {
1848 chunks.set(id, createResolvedModelChunk(response, model));
1849 } else {
1865 - resolveModelChunk(chunk, model);
1850 + resolveModelChunk(response, chunk, model);
1851 }
1852 }
1853
@@ -2036,7 +2021,7 @@ function startReadableStream<T>(
2021 // to synchronous emitting.
2022 previousBlockedChunk = null;
2023 }
2039 - resolveModelChunk(chunk, json);
2024 + resolveModelChunk(response, chunk, json);
2025 });
2026 }
2027 },
@@ -2124,7 +2109,12 @@ function startAsyncIterable<T>(
2109 false,
2110 );
2111 } else {
2127 - resolveIteratorResultChunk(buffer[nextWriteIndex], value, false);
2112 + resolveIteratorResultChunk(
2113 + response,
2114 + buffer[nextWriteIndex],
2115 + value,
2116 + false,
2117 + );
2118 }
2119 nextWriteIndex++;
2120 },
@@ -2137,12 +2127,18 @@ function startAsyncIterable<T>(
2127 true,
2128 );
2129 } else {
2140 - resolveIteratorResultChunk(buffer[nextWriteIndex], value, true);
2130 + resolveIteratorResultChunk(
2131 + response,
2132 + buffer[nextWriteIndex],
2133 + value,
2134 + true,
2135 + );
2136 }
2137 nextWriteIndex++;
2138 while (nextWriteIndex < buffer.length) {
2139 // In generators, any extra reads from the iterator have the value undefined.
2140 resolveIteratorResultChunk(
2141 + response,
2142 buffer[nextWriteIndex++],
2143 '"$undefined"',
2144 true,
@@ -2178,7 +2174,6 @@ function startAsyncIterable<T>(
2174 INITIALIZED,
2175 {done: true, value: undefined},
2176 null,
2181 - response,
2177 );
2178 }
2179 buffer[nextReadIndex] =
@@ -2946,7 +2941,7 @@ function resolveIOInfo(
2941 chunks.set(id, chunk);
2942 initializeModelChunk(chunk);
2943 } else {
2949 - resolveModelChunk(chunk, model);
2944 + resolveModelChunk(response, chunk, model);
2945 if (chunk.status === RESOLVED_MODEL) {
2946 initializeModelChunk(chunk);
2947 }