@samitouri / QOS-React / commits / 01a40570c3

[Flight/Fizz] Use Constructors for Large Request/Response Objects in Flight/Fizz (#29858)

We know from Fiber that inline objects with more than 16 properties in V8 turn into dictionaries instead of optimized objects. The trick is to use a constructor instead of an inline object literal. I don't actually know if that's still the case or not. I haven't benchmarked/tested the output. Better safe than sorry. It's unfortunate that this can have a negative effect for Hermes and JSC but it's not as bad as it is for V8 because they don't deopt into dictionaries. The time to construct these objects isn't a concern - the time to access them frequently is. We have to beware the Task objects in Fizz. Those are currently on 16 fields exactly so we shouldn't add anymore ideally. We should ideally have a lint rule against object literals with more than 16 fields on them. It might not help since sometimes the fields are conditional.

Sebastian Markbåge committed Jun 11, 2024 at 15:55 UTC 01a40570c3cd852593c9bc88978b11cb9a2c5720
3 files changed +165 -92
packages/react-client/src/ReactFlightClient.js
+41 -22
@@ -1136,7 +1136,8 @@ function missingCall() {
1136 );
1137 }
1138
1139 -export function createResponse(
1139 +function ResponseInstance(
1140 + this: $FlowFixMe,
1141 bundlerConfig: SSRModuleMap,
1142 moduleLoading: ModuleLoading,
1143 callServer: void | CallServerCallback,
@@ -1144,38 +1145,56 @@ export function createResponse(
1145 nonce: void | string,
1146 temporaryReferences: void | TemporaryReferenceSet,
1147 findSourceMapURL: void | FindSourceMapURLCallback,
1147 -): Response {
1148 +) {
1149 const chunks: Map<number, SomeChunk<any>> = new Map();
1149 - const response: Response = {
1150 - _bundlerConfig: bundlerConfig,
1151 - _moduleLoading: moduleLoading,
1152 - _callServer: callServer !== undefined ? callServer : missingCall,
1153 - _encodeFormAction: encodeFormAction,
1154 - _nonce: nonce,
1155 - _chunks: chunks,
1156 - _stringDecoder: createStringDecoder(),
1157 - _fromJSON: (null: any),
1158 - _rowState: 0,
1159 - _rowID: 0,
1160 - _rowTag: 0,
1161 - _rowLength: 0,
1162 - _buffer: [],
1163 - _tempRefs: temporaryReferences,
1164 - };
1150 + this._bundlerConfig = bundlerConfig;
1151 + this._moduleLoading = moduleLoading;
1152 + this._callServer = callServer !== undefined ? callServer : missingCall;
1153 + this._encodeFormAction = encodeFormAction;
1154 + this._nonce = nonce;
1155 + this._chunks = chunks;
1156 + this._stringDecoder = createStringDecoder();
1157 + this._fromJSON = (null: any);
1158 + this._rowState = 0;
1159 + this._rowID = 0;
1160 + this._rowTag = 0;
1161 + this._rowLength = 0;
1162 + this._buffer = [];
1163 + this._tempRefs = temporaryReferences;
1164 if (supportsCreateTask) {
1165 // Any stacks that appear on the server need to be rooted somehow on the client
1166 // so we create a root Task for this response which will be the root owner for any
1167 // elements created by the server. We use the "use server" string to indicate that
1168 // this is where we enter the server from the client.
1169 // TODO: Make this string configurable.
1171 - response._debugRootTask = (console: any).createTask('"use server"');
1170 + this._debugRootTask = (console: any).createTask('"use server"');
1171 }
1172 if (__DEV__) {
1174 - response._debugFindSourceMapURL = findSourceMapURL;
1173 + this._debugFindSourceMapURL = findSourceMapURL;
1174 }
1175 // Don't inline this call because it causes closure to outline the call above.
1177 - response._fromJSON = createFromJSONCallback(response);
1178 - return response;
1176 + this._fromJSON = createFromJSONCallback(this);
1177 +}
1178 +
1179 +export function createResponse(
1180 + bundlerConfig: SSRModuleMap,
1181 + moduleLoading: ModuleLoading,
1182 + callServer: void | CallServerCallback,
1183 + encodeFormAction: void | EncodeFormActionCallback,
1184 + nonce: void | string,
1185 + temporaryReferences: void | TemporaryReferenceSet,
1186 + findSourceMapURL: void | FindSourceMapURLCallback,
1187 +): Response {
1188 + // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
1189 + return new ResponseInstance(
1190 + bundlerConfig,
1191 + moduleLoading,
1192 + callServer,
1193 + encodeFormAction,
1194 + nonce,
1195 + temporaryReferences,
1196 + findSourceMapURL,
1197 + );
1198 }
1199
1200 function resolveModel(
packages/react-server/src/ReactFizzServer.js
+69 -36
@@ -237,6 +237,8 @@ type RenderTask = {
237 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
238 thenableState: null | ThenableState,
239 isFallback: boolean, // whether this task is rendering inside a fallback tree
240 + // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
241 + // Consider splitting into multiple objects or consolidating some fields.
242 };
243
244 type ReplaySet = {
@@ -264,6 +266,8 @@ type ReplayTask = {
266 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
267 thenableState: null | ThenableState,
268 isFallback: boolean, // whether this task is rendering inside a fallback tree
269 + // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
270 + // Consider splitting into multiple objects or consolidating some fields.
271 };
272
273 export type Task = RenderTask | ReplayTask;
@@ -365,7 +369,8 @@ function defaultErrorHandler(error: mixed) {
369
370 function noop(): void {}
371
368 -export function createRequest(
372 +function RequestInstance(
373 + this: $FlowFixMe,
374 children: ReactNodeList,
375 resumableState: ResumableState,
376 renderState: RenderState,
@@ -378,45 +383,43 @@ export function createRequest(
383 onFatalError: void | ((error: mixed) => void),
384 onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
385 formState: void | null | ReactFormState<any, any>,
381 -): Request {
386 +) {
387 const pingedTasks: Array<Task> = [];
388 const abortSet: Set<Task> = new Set();
384 - const request: Request = {
385 - destination: null,
386 - flushScheduled: false,
387 - resumableState,
388 - renderState,
389 - rootFormatContext,
390 - progressiveChunkSize:
391 - progressiveChunkSize === undefined
392 - ? DEFAULT_PROGRESSIVE_CHUNK_SIZE
393 - : progressiveChunkSize,
394 - status: OPEN,
395 - fatalError: null,
396 - nextSegmentId: 0,
397 - allPendingTasks: 0,
398 - pendingRootTasks: 0,
399 - completedRootSegment: null,
400 - abortableTasks: abortSet,
401 - pingedTasks: pingedTasks,
402 - clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
403 - completedBoundaries: ([]: Array<SuspenseBoundary>),
404 - partialBoundaries: ([]: Array<SuspenseBoundary>),
405 - trackedPostpones: null,
406 - onError: onError === undefined ? defaultErrorHandler : onError,
407 - onPostpone: onPostpone === undefined ? noop : onPostpone,
408 - onAllReady: onAllReady === undefined ? noop : onAllReady,
409 - onShellReady: onShellReady === undefined ? noop : onShellReady,
410 - onShellError: onShellError === undefined ? noop : onShellError,
411 - onFatalError: onFatalError === undefined ? noop : onFatalError,
412 - formState: formState === undefined ? null : formState,
413 - };
389 + this.destination = null;
390 + this.flushScheduled = false;
391 + this.resumableState = resumableState;
392 + this.renderState = renderState;
393 + this.rootFormatContext = rootFormatContext;
394 + this.progressiveChunkSize =
395 + progressiveChunkSize === undefined
396 + ? DEFAULT_PROGRESSIVE_CHUNK_SIZE
397 + : progressiveChunkSize;
398 + this.status = OPEN;
399 + this.fatalError = null;
400 + this.nextSegmentId = 0;
401 + this.allPendingTasks = 0;
402 + this.pendingRootTasks = 0;
403 + this.completedRootSegment = null;
404 + this.abortableTasks = abortSet;
405 + this.pingedTasks = pingedTasks;
406 + this.clientRenderedBoundaries = ([]: Array<SuspenseBoundary>);
407 + this.completedBoundaries = ([]: Array<SuspenseBoundary>);
408 + this.partialBoundaries = ([]: Array<SuspenseBoundary>);
409 + this.trackedPostpones = null;
410 + this.onError = onError === undefined ? defaultErrorHandler : onError;
411 + this.onPostpone = onPostpone === undefined ? noop : onPostpone;
412 + this.onAllReady = onAllReady === undefined ? noop : onAllReady;
413 + this.onShellReady = onShellReady === undefined ? noop : onShellReady;
414 + this.onShellError = onShellError === undefined ? noop : onShellError;
415 + this.onFatalError = onFatalError === undefined ? noop : onFatalError;
416 + this.formState = formState === undefined ? null : formState;
417 if (__DEV__) {
415 - request.didWarnForKey = null;
418 + this.didWarnForKey = null;
419 }
420 // This segment represents the root fallback.
421 const rootSegment = createPendingSegment(
419 - request,
422 + this,
423 0,
424 null,
425 rootFormatContext,
@@ -427,7 +430,7 @@ export function createRequest(
430 // There is no parent so conceptually, we're unblocked to flush this segment.
431 rootSegment.parentFlushed = true;
432 const rootTask = createRenderTask(
430 - request,
433 + this,
434 null,
435 children,
436 -1,
@@ -444,7 +447,37 @@ export function createRequest(
447 false,
448 );
449 pingedTasks.push(rootTask);
447 - return request;
450 +}
451 +
452 +export function createRequest(
453 + children: ReactNodeList,
454 + resumableState: ResumableState,
455 + renderState: RenderState,
456 + rootFormatContext: FormatContext,
457 + progressiveChunkSize: void | number,
458 + onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),
459 + onAllReady: void | (() => void),
460 + onShellReady: void | (() => void),
461 + onShellError: void | ((error: mixed) => void),
462 + onFatalError: void | ((error: mixed) => void),
463 + onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
464 + formState: void | null | ReactFormState<any, any>,
465 +): Request {
466 + // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
467 + return new RequestInstance(
468 + children,
469 + resumableState,
470 + renderState,
471 + rootFormatContext,
472 + progressiveChunkSize,
473 + onError,
474 + onAllReady,
475 + onShellReady,
476 + onShellError,
477 + onFatalError,
478 + onPostpone,
479 + formState,
480 + );
481 }
482
483 export function createPrerenderRequest(
packages/react-server/src/ReactFlightServer.js
+55 -34
@@ -473,7 +473,8 @@ const ABORTING = 1;
473 const CLOSING = 2;
474 const CLOSED = 3;
475
476 -export function createRequest(
476 +function RequestInstance(
477 + this: $FlowFixMe,
478 model: ReactClientValue,
479 bundlerConfig: ClientManifest,
480 onError: void | ((error: mixed) => ?string),
@@ -481,7 +482,7 @@ export function createRequest(
482 onPostpone: void | ((reason: string) => void),
483 environmentName: void | string,
484 temporaryReferences: void | TemporaryReferenceSet,
484 -): Request {
485 +) {
486 if (
487 ReactSharedInternals.A !== null &&
488 ReactSharedInternals.A !== DefaultAsyncDispatcher
@@ -499,42 +500,62 @@ export function createRequest(
500 TaintRegistryPendingRequests.add(cleanupQueue);
501 }
502 const hints = createHints();
502 - const request: Request = ({
503 - status: OPEN,
504 - flushScheduled: false,
505 - fatalError: null,
506 - destination: null,
507 - bundlerConfig,
508 - cache: new Map(),
509 - nextChunkId: 0,
510 - pendingChunks: 0,
511 - hints,
512 - abortListeners: new Set(),
513 - abortableTasks: abortSet,
514 - pingedTasks: pingedTasks,
515 - completedImportChunks: ([]: Array<Chunk>),
516 - completedHintChunks: ([]: Array<Chunk>),
517 - completedRegularChunks: ([]: Array<Chunk | BinaryChunk>),
518 - completedErrorChunks: ([]: Array<Chunk>),
519 - writtenSymbols: new Map(),
520 - writtenClientReferences: new Map(),
521 - writtenServerReferences: new Map(),
522 - writtenObjects: new WeakMap(),
523 - temporaryReferences: temporaryReferences,
524 - identifierPrefix: identifierPrefix || '',
525 - identifierCount: 1,
526 - taintCleanupQueue: cleanupQueue,
527 - onError: onError === undefined ? defaultErrorHandler : onError,
528 - onPostpone: onPostpone === undefined ? defaultPostponeHandler : onPostpone,
529 - }: any);
503 + this.status = OPEN;
504 + this.flushScheduled = false;
505 + this.fatalError = null;
506 + this.destination = null;
507 + this.bundlerConfig = bundlerConfig;
508 + this.cache = new Map();
509 + this.nextChunkId = 0;
510 + this.pendingChunks = 0;
511 + this.hints = hints;
512 + this.abortListeners = new Set();
513 + this.abortableTasks = abortSet;
514 + this.pingedTasks = pingedTasks;
515 + this.completedImportChunks = ([]: Array<Chunk>);
516 + this.completedHintChunks = ([]: Array<Chunk>);
517 + this.completedRegularChunks = ([]: Array<Chunk | BinaryChunk>);
518 + this.completedErrorChunks = ([]: Array<Chunk>);
519 + this.writtenSymbols = new Map();
520 + this.writtenClientReferences = new Map();
521 + this.writtenServerReferences = new Map();
522 + this.writtenObjects = new WeakMap();
523 + this.temporaryReferences = temporaryReferences;
524 + this.identifierPrefix = identifierPrefix || '';
525 + this.identifierCount = 1;
526 + this.taintCleanupQueue = cleanupQueue;
527 + this.onError = onError === undefined ? defaultErrorHandler : onError;
528 + this.onPostpone =
529 + onPostpone === undefined ? defaultPostponeHandler : onPostpone;
530 +
531 if (__DEV__) {
531 - request.environmentName =
532 + this.environmentName =
533 environmentName === undefined ? 'Server' : environmentName;
533 - request.didWarnForKey = null;
534 + this.didWarnForKey = null;
535 }
535 - const rootTask = createTask(request, model, null, false, abortSet);
536 + const rootTask = createTask(this, model, null, false, abortSet);
537 pingedTasks.push(rootTask);
537 - return request;
538 +}
539 +
540 +export function createRequest(
541 + model: ReactClientValue,
542 + bundlerConfig: ClientManifest,
543 + onError: void | ((error: mixed) => ?string),
544 + identifierPrefix?: string,
545 + onPostpone: void | ((reason: string) => void),
546 + environmentName: void | string,
547 + temporaryReferences: void | TemporaryReferenceSet,
548 +): Request {
549 + // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
550 + return new RequestInstance(
551 + model,
552 + bundlerConfig,
553 + onError,
554 + identifierPrefix,
555 + onPostpone,
556 + environmentName,
557 + temporaryReferences,
558 + );
559 }
560
561 let currentRequest: null | Request = null;