Repository: facebook/react
Pull request: [Flight/Fizz] Use Constructors for Large Request/Response Objects in Flight/Fizz

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.

Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -1136,46 +1136,65 @@ function missingCall() {
   );
 }
 
-export function createResponse(
+function ResponseInstance(
+  this: $FlowFixMe,
   bundlerConfig: SSRModuleMap,
   moduleLoading: ModuleLoading,
   callServer: void | CallServerCallback,
   encodeFormAction: void | EncodeFormActionCallback,
   nonce: void | string,
   temporaryReferences: void | TemporaryReferenceSet,
   findSourceMapURL: void | FindSourceMapURLCallback,
-): Response {
+) {
   const chunks: Map<number, SomeChunk<any>> = new Map();
-  const response: Response = {
-    _bundlerConfig: bundlerConfig,
-    _moduleLoading: moduleLoading,
-    _callServer: callServer !== undefined ? callServer : missingCall,
-    _encodeFormAction: encodeFormAction,
-    _nonce: nonce,
-    _chunks: chunks,
-    _stringDecoder: createStringDecoder(),
-    _fromJSON: (null: any),
-    _rowState: 0,
-    _rowID: 0,
-    _rowTag: 0,
-    _rowLength: 0,
-    _buffer: [],
-    _tempRefs: temporaryReferences,
-  };
+  this._bundlerConfig = bundlerConfig;
+  this._moduleLoading = moduleLoading;
+  this._callServer = callServer !== undefined ? callServer : missingCall;
+  this._encodeFormAction = encodeFormAction;
+  this._nonce = nonce;
+  this._chunks = chunks;
+  this._stringDecoder = createStringDecoder();
+  this._fromJSON = (null: any);
+  this._rowState = 0;
+  this._rowID = 0;
+  this._rowTag = 0;
+  this._rowLength = 0;
+  this._buffer = [];
+  this._tempRefs = temporaryReferences;
   if (supportsCreateTask) {
     // Any stacks that appear on the server need to be rooted somehow on the client
     // so we create a root Task for this response which will be the root owner for any
     // elements created by the server. We use the "use server" string to indicate that
     // this is where we enter the server from the client.
     // TODO: Make this string configurable.
-    response._debugRootTask = (console: any).createTask('"use server"');
+    this._debugRootTask = (console: any).createTask('"use server"');
   }
   if (__DEV__) {
-    response._debugFindSourceMapURL = findSourceMapURL;
+    this._debugFindSourceMapURL = findSourceMapURL;
   }
   // Don't inline this call because it causes closure to outline the call above.
-  response._fromJSON = createFromJSONCallback(response);
-  return response;
+  this._fromJSON = createFromJSONCallback(this);
+}
+
+export function createResponse(
+  bundlerConfig: SSRModuleMap,
+  moduleLoading: ModuleLoading,
+  callServer: void | CallServerCallback,
+  encodeFormAction: void | EncodeFormActionCallback,
+  nonce: void | string,
+  temporaryReferences: void | TemporaryReferenceSet,
+  findSourceMapURL: void | FindSourceMapURLCallback,
+): Response {
+  // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
+  return new ResponseInstance(
+    bundlerConfig,
+    moduleLoading,
+    callServer,
+    encodeFormAction,
+    nonce,
+    temporaryReferences,
+    findSourceMapURL,
+  );
 }
 
 function resolveModel(

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -237,6 +237,8 @@ type RenderTask = {
   componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
   thenableState: null | ThenableState,
   isFallback: boolean, // whether this task is rendering inside a fallback tree
+  // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
+  // Consider splitting into multiple objects or consolidating some fields.
 };
 
 type ReplaySet = {
@@ -264,6 +266,8 @@ type ReplayTask = {
   componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
   thenableState: null | ThenableState,
   isFallback: boolean, // whether this task is rendering inside a fallback tree
+  // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor.
+  // Consider splitting into multiple objects or consolidating some fields.
 };
 
 export type Task = RenderTask | ReplayTask;
@@ -365,7 +369,8 @@ function defaultErrorHandler(error: mixed) {
 
 function noop(): void {}
 
-export function createRequest(
+function RequestInstance(
+  this: $FlowFixMe,
   children: ReactNodeList,
   resumableState: ResumableState,
   renderState: RenderState,
@@ -378,45 +383,43 @@ export function createRequest(
   onFatalError: void | ((error: mixed) => void),
   onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
   formState: void | null | ReactFormState<any, any>,
-): Request {
+) {
   const pingedTasks: Array<Task> = [];
   const abortSet: Set<Task> = new Set();
-  const request: Request = {
-    destination: null,
-    flushScheduled: false,
-    resumableState,
-    renderState,
-    rootFormatContext,
-    progressiveChunkSize:
-      progressiveChunkSize === undefined
-        ? DEFAULT_PROGRESSIVE_CHUNK_SIZE
-        : progressiveChunkSize,
-    status: OPEN,
-    fatalError: null,
-    nextSegmentId: 0,
-    allPendingTasks: 0,
-    pendingRootTasks: 0,
-    completedRootSegment: null,
-    abortableTasks: abortSet,
-    pingedTasks: pingedTasks,
-    clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
-    completedBoundaries: ([]: Array<SuspenseBoundary>),
-    partialBoundaries: ([]: Array<SuspenseBoundary>),
-    trackedPostpones: null,
-    onError: onError === undefined ? defaultErrorHandler : onError,
-    onPostpone: onPostpone === undefined ? noop : onPostpone,
-    onAllReady: onAllReady === undefined ? noop : onAllReady,
-    onShellReady: onShellReady === undefined ? noop : onShellReady,
-    onShellError: onShellError === undefined ? noop : onShellError,
-    onFatalError: onFatalError === undefined ? noop : onFatalError,
-    formState: formState === undefined ? null : formState,
-  };
+  this.destination = null;
+  this.flushScheduled = false;
+  this.resumableState = resumableState;
+  this.renderState = renderState;
+  this.rootFormatContext = rootFormatContext;
+  this.progressiveChunkSize =
+    progressiveChunkSize === undefined
+      ? DEFAULT_PROGRESSIVE_CHUNK_SIZE
+      : progressiveChunkSize;
+  this.status = OPEN;
+  this.fatalError = null;
+  this.nextSegmentId = 0;
+  this.allPendingTasks = 0;
+  this.pendingRootTasks = 0;
+  this.completedRootSegment = null;
+  this.abortableTasks = abortSet;
+  this.pingedTasks = pingedTasks;
+  this.clientRenderedBoundaries = ([]: Array<SuspenseBoundary>);
+  this.completedBoundaries = ([]: Array<SuspenseBoundary>);
+  this.partialBoundaries = ([]: Array<SuspenseBoundary>);
+  this.trackedPostpones = null;
+  this.onError = onError === undefined ? defaultErrorHandler : onError;
+  this.onPostpone = onPostpone === undefined ? noop : onPostpone;
+  this.onAllReady = onAllReady === undefined ? noop : onAllReady;
+  this.onShellReady = onShellReady === undefined ? noop : onShellReady;
+  this.onShellError = onShellError === undefined ? noop : onShellError;
+  this.onFatalError = onFatalError === undefined ? noop : onFatalError;
+  this.formState = formState === undefined ? null : formState;
   if (__DEV__) {
-    request.didWarnForKey = null;
+    this.didWarnForKey = null;
   }
   // This segment represents the root fallback.
   const rootSegment = createPendingSegment(
-    request,
+    this,
     0,
     null,
     rootFormatContext,
@@ -427,7 +430,7 @@ export function createRequest(
   // There is no parent so conceptually, we're unblocked to flush this segment.
   rootSegment.parentFlushed = true;
   const rootTask = createRenderTask(
-    request,
+    this,
     null,
     children,
     -1,
@@ -444,7 +447,37 @@ export function createRequest(
     false,
   );
   pingedTasks.push(rootTask);
-  return request;
+}
+
+export function createRequest(
+  children: ReactNodeList,
+  resumableState: ResumableState,
+  renderState: RenderState,
+  rootFormatContext: FormatContext,
+  progressiveChunkSize: void | number,
+  onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),
+  onAllReady: void | (() => void),
+  onShellReady: void | (() => void),
+  onShellError: void | ((error: mixed) => void),
+  onFatalError: void | ((error: mixed) => void),
+  onPostpone: void | ((reason: string, postponeInfo: PostponeInfo) => void),
+  formState: void | null | ReactFormState<any, any>,
+): Request {
+  // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
+  return new RequestInstance(
+    children,
+    resumableState,
+    renderState,
+    rootFormatContext,
+    progressiveChunkSize,
+    onError,
+    onAllReady,
+    onShellReady,
+    onShellError,
+    onFatalError,
+    onPostpone,
+    formState,
+  );
 }
 
 export function createPrerenderRequest(

filename: packages/react-server/src/ReactFlightServer.js
patch: @@ -473,15 +473,16 @@ const ABORTING = 1;
 const CLOSING = 2;
 const CLOSED = 3;
 
-export function createRequest(
+function RequestInstance(
+  this: $FlowFixMe,
   model: ReactClientValue,
   bundlerConfig: ClientManifest,
   onError: void | ((error: mixed) => ?string),
   identifierPrefix?: string,
   onPostpone: void | ((reason: string) => void),
   environmentName: void | string,
   temporaryReferences: void | TemporaryReferenceSet,
-): Request {
+) {
   if (
     ReactSharedInternals.A !== null &&
     ReactSharedInternals.A !== DefaultAsyncDispatcher
@@ -499,42 +500,62 @@ export function createRequest(
     TaintRegistryPendingRequests.add(cleanupQueue);
   }
   const hints = createHints();
-  const request: Request = ({
-    status: OPEN,
-    flushScheduled: false,
-    fatalError: null,
-    destination: null,
-    bundlerConfig,
-    cache: new Map(),
-    nextChunkId: 0,
-    pendingChunks: 0,
-    hints,
-    abortListeners: new Set(),
-    abortableTasks: abortSet,
-    pingedTasks: pingedTasks,
-    completedImportChunks: ([]: Array<Chunk>),
-    completedHintChunks: ([]: Array<Chunk>),
-    completedRegularChunks: ([]: Array<Chunk | BinaryChunk>),
-    completedErrorChunks: ([]: Array<Chunk>),
-    writtenSymbols: new Map(),
-    writtenClientReferences: new Map(),
-    writtenServerReferences: new Map(),
-    writtenObjects: new WeakMap(),
-    temporaryReferences: temporaryReferences,
-    identifierPrefix: identifierPrefix || '',
-    identifierCount: 1,
-    taintCleanupQueue: cleanupQueue,
-    onError: onError === undefined ? defaultErrorHandler : onError,
-    onPostpone: onPostpone === undefined ? defaultPostponeHandler : onPostpone,
-  }: any);
+  this.status = OPEN;
+  this.flushScheduled = false;
+  this.fatalError = null;
+  this.destination = null;
+  this.bundlerConfig = bundlerConfig;
+  this.cache = new Map();
+  this.nextChunkId = 0;
+  this.pendingChunks = 0;
+  this.hints = hints;
+  this.abortListeners = new Set();
+  this.abortableTasks = abortSet;
+  this.pingedTasks = pingedTasks;
+  this.completedImportChunks = ([]: Array<Chunk>);
+  this.completedHintChunks = ([]: Array<Chunk>);
+  this.completedRegularChunks = ([]: Array<Chunk | BinaryChunk>);
+  this.completedErrorChunks = ([]: Array<Chunk>);
+  this.writtenSymbols = new Map();
+  this.writtenClientReferences = new Map();
+  this.writtenServerReferences = new Map();
+  this.writtenObjects = new WeakMap();
+  this.temporaryReferences = temporaryReferences;
+  this.identifierPrefix = identifierPrefix || '';
+  this.identifierCount = 1;
+  this.taintCleanupQueue = cleanupQueue;
+  this.onError = onError === undefined ? defaultErrorHandler : onError;
+  this.onPostpone =
+    onPostpone === undefined ? defaultPostponeHandler : onPostpone;
+
   if (__DEV__) {
-    request.environmentName =
+    this.environmentName =
       environmentName === undefined ? 'Server' : environmentName;
-    request.didWarnForKey = null;
+    this.didWarnForKey = null;
   }
-  const rootTask = createTask(request, model, null, false, abortSet);
+  const rootTask = createTask(this, model, null, false, abortSet);
   pingedTasks.push(rootTask);
-  return request;
+}
+
+export function createRequest(
+  model: ReactClientValue,
+  bundlerConfig: ClientManifest,
+  onError: void | ((error: mixed) => ?string),
+  identifierPrefix?: string,
+  onPostpone: void | ((reason: string) => void),
+  environmentName: void | string,
+  temporaryReferences: void | TemporaryReferenceSet,
+): Request {
+  // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
+  return new RequestInstance(
+    model,
+    bundlerConfig,
+    onError,
+    identifierPrefix,
+    onPostpone,
+    environmentName,
+    temporaryReferences,
+  );
 }
 
 let currentRequest: null | Request = null;