Repository: facebook/react
Pull request: Make class prop resolution faster

`delete` causes an object (in V8, and maybe other engines) to deopt to a dictionary instead of a class. Instead of `assign` + `delete`, manually iterate over all the properties, like the JSX runtime does.

To avoid copying the object twice I moved the `ref` prop removal to come before handling default props. If we already cloned the props to remove `ref`, then we can skip cloning again to handle default props.

Changed files:
filename: packages/react-reconciler/src/ReactFiberClassComponent.js
patch: @@ -1251,32 +1251,39 @@ export function resolveClassComponentProps(
 ): Object {
   let newProps = baseProps;
 
-  // Resolve default props. Taken from old JSX runtime, where this used to live.
+  if (enableRefAsProp) {
+    // Remove ref from the props object, if it exists.
+    if ('ref' in baseProps) {
+      newProps = ({}: any);
+      for (const propName in baseProps) {
+        if (propName !== 'ref') {
+          newProps[propName] = baseProps[propName];
+        }
+      }
+    }
+  }
+
+  // Resolve default props.
   const defaultProps = Component.defaultProps;
   if (
     defaultProps &&
     // If disableDefaultPropsExceptForClasses is true, we always resolve
     // default props here in the reconciler, rather than in the JSX runtime.
     (disableDefaultPropsExceptForClasses || !alreadyResolvedDefaultProps)
   ) {
-    newProps = assign({}, newProps, baseProps);
+    // We may have already copied the props object above to remove ref. If so,
+    // we can modify that. Otherwise, copy the props object with Object.assign.
+    if (newProps === baseProps) {
+      newProps = assign({}, newProps, baseProps);
+    }
+    // Taken from old JSX runtime, where this used to live.
     for (const propName in defaultProps) {
       if (newProps[propName] === undefined) {
         newProps[propName] = defaultProps[propName];
       }
     }
   }
 
-  if (enableRefAsProp) {
-    // Remove ref from the props object, if it exists.
-    if ('ref' in newProps) {
-      if (newProps === baseProps) {
-        newProps = assign({}, newProps);
-      }
-      delete newProps.ref;
-    }
-  }
-
   return newProps;
 }
 

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -1397,24 +1397,36 @@ export function resolveClassComponentProps(
 ): Object {
   let newProps = baseProps;
 
-  // Resolve default props. Taken from old JSX runtime, where this used to live.
-  const defaultProps = Component.defaultProps;
-  if (defaultProps && disableDefaultPropsExceptForClasses) {
-    newProps = assign({}, newProps, baseProps);
-    for (const propName in defaultProps) {
-      if (newProps[propName] === undefined) {
-        newProps[propName] = defaultProps[propName];
+  if (enableRefAsProp) {
+    // Remove ref from the props object, if it exists.
+    if ('ref' in baseProps) {
+      newProps = ({}: any);
+      for (const propName in baseProps) {
+        if (propName !== 'ref') {
+          newProps[propName] = baseProps[propName];
+        }
       }
     }
   }
 
-  if (enableRefAsProp) {
-    // Remove ref from the props object, if it exists.
-    if ('ref' in newProps) {
-      if (newProps === baseProps) {
-        newProps = assign({}, newProps);
+  // Resolve default props.
+  const defaultProps = Component.defaultProps;
+  if (
+    defaultProps &&
+    // If disableDefaultPropsExceptForClasses is true, we always resolve
+    // default props here, rather than in the JSX runtime.
+    disableDefaultPropsExceptForClasses
+  ) {
+    // We may have already copied the props object above to remove ref. If so,
+    // we can modify that. Otherwise, copy the props object with Object.assign.
+    if (newProps === baseProps) {
+      newProps = assign({}, newProps, baseProps);
+    }
+    // Taken from old JSX runtime, where this used to live.
+    for (const propName in defaultProps) {
+      if (newProps[propName] === undefined) {
+        newProps[propName] = defaultProps[propName];
       }
-      delete newProps.ref;
     }
   }