Repository: facebook/react
Pull request: Include Component Props in Performance Track

Similar to how we can include a Promise resolved value we can include Component Props.

For now I left out props for Client Components for perf unless they error. I'll try it for Client Components in general in a separate PR.

<img width="730" alt="Screenshot 2025-06-26 at 5 54 29 PM" src="https://github.com/user-attachments/assets/f0c86911-2899-4b5f-b45f-5326bdbc630f" />
<img width="762" alt="Screenshot 2025-06-26 at 5 54 12 PM" src="https://github.com/user-attachments/assets/97540d19-5950-4346-99e6-066af086040e" />


Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -100,7 +100,7 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
 
 import {injectInternals} from './ReactFlightClientDevToolsHook';
 
-import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
+import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
 
 import ReactVersion from 'shared/ReactVersion';
 

filename: packages/react-client/src/ReactFlightPerformanceTrack.js
patch: @@ -17,10 +17,10 @@ import type {
 
 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
 
-import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
-
-import hasOwnProperty from 'shared/hasOwnProperty';
-import isArray from 'shared/isArray';
+import {
+  addValueToProperties,
+  addObjectToProperties,
+} from 'shared/ReactPerformanceTrackProperties';
 
 const supportsUserTiming =
   enableProfilerTimer &&
@@ -33,127 +33,6 @@ const supportsUserTiming =
 const IO_TRACK = 'Server Requests ⚛';
 const COMPONENTS_TRACK = 'Server Components ⚛';
 
-const EMPTY_ARRAY = 0;
-const COMPLEX_ARRAY = 1;
-const PRIMITIVE_ARRAY = 2; // Primitive values only
-const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
-function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
-  let kind = EMPTY_ARRAY;
-  for (let i = 0; i < array.length; i++) {
-    const value = array[i];
-    if (typeof value === 'object' && value !== null) {
-      if (
-        isArray(value) &&
-        value.length === 2 &&
-        typeof value[0] === 'string'
-      ) {
-        // Key value tuple
-        if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
-          return COMPLEX_ARRAY;
-        }
-        kind = ENTRIES_ARRAY;
-      } else {
-        return COMPLEX_ARRAY;
-      }
-    } else if (typeof value === 'function') {
-      return COMPLEX_ARRAY;
-    } else if (typeof value === 'string' && value.length > 50) {
-      return COMPLEX_ARRAY;
-    } else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
-      return COMPLEX_ARRAY;
-    } else {
-      kind = PRIMITIVE_ARRAY;
-    }
-  }
-  return kind;
-}
-
-function addObjectToProperties(
-  object: Object,
-  properties: Array<[string, string]>,
-  indent: number,
-): void {
-  for (const key in object) {
-    if (hasOwnProperty.call(object, key) && key[0] !== '_') {
-      const value = object[key];
-      addValueToProperties(key, value, properties, indent);
-    }
-  }
-}
-
-function addValueToProperties(
-  propertyName: string,
-  value: mixed,
-  properties: Array<[string, string]>,
-  indent: number,
-): void {
-  let desc;
-  switch (typeof value) {
-    case 'object':
-      if (value === null) {
-        desc = 'null';
-        break;
-      } else {
-        // $FlowFixMe[method-unbinding]
-        const objectToString = Object.prototype.toString.call(value);
-        let objectName = objectToString.slice(8, objectToString.length - 1);
-        if (objectName === 'Array') {
-          const array: Array<any> = (value: any);
-          const kind = getArrayKind(array);
-          if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
-            desc = JSON.stringify(array);
-            break;
-          } else if (kind === ENTRIES_ARRAY) {
-            properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
-            for (let i = 0; i < array.length; i++) {
-              const entry = array[i];
-              addValueToProperties(entry[0], entry[1], properties, indent + 1);
-            }
-            return;
-          }
-        }
-        if (objectName === 'Object') {
-          const proto: any = Object.getPrototypeOf(value);
-          if (proto && typeof proto.constructor === 'function') {
-            objectName = proto.constructor.name;
-          }
-        }
-        properties.push([
-          '\xa0\xa0'.repeat(indent) + propertyName,
-          objectName === 'Object' ? '' : objectName,
-        ]);
-        if (indent < 3) {
-          addObjectToProperties(value, properties, indent + 1);
-        }
-        return;
-      }
-    case 'function':
-      if (value.name === '') {
-        desc = '() => {}';
-      } else {
-        desc = value.name + '() {}';
-      }
-      break;
-    case 'string':
-      if (value === OMITTED_PROP_ERROR) {
-        desc = '...';
-      } else {
-        desc = JSON.stringify(value);
-      }
-      break;
-    case 'undefined':
-      desc = 'undefined';
-      break;
-    case 'boolean':
-      desc = value ? 'true' : 'false';
-      break;
-    default:
-      // eslint-disable-next-line react-internal/safe-string-coercion
-      desc = String(value);
-  }
-  properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
-}
-
 export function markAllTracksInOrder() {
   if (supportsUserTiming) {
     // Ensure we create the Server Component track groups earlier than the Client Scheduler
@@ -222,17 +101,27 @@ export function logComponentRender(
       isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
     const debugTask = componentInfo.debugTask;
     if (__DEV__ && debugTask) {
+      const properties: Array<[string, string]> = [];
+      if (componentInfo.key != null) {
+        addValueToProperties('key', componentInfo.key, properties, 0);
+      }
+      if (componentInfo.props != null) {
+        addObjectToProperties(componentInfo.props, properties, 0);
+      }
       debugTask.run(
         // $FlowFixMe[method-unbinding]
-        console.timeStamp.bind(
-          console,
-          entryName,
-          startTime < 0 ? 0 : startTime,
-          childrenEndTime,
-          trackNames[trackIdx],
-          COMPONENTS_TRACK,
-          color,
-        ),
+        performance.measure.bind(performance, entryName, {
+          start: startTime < 0 ? 0 : startTime,
+          end: childrenEndTime,
+          detail: {
+            devtools: {
+              color: color,
+              track: trackNames[trackIdx],
+              trackGroup: COMPONENTS_TRACK,
+              properties,
+            },
+          },
+        }),
       );
     } else {
       console.timeStamp(
@@ -268,6 +157,12 @@ export function logComponentAborted(
           'The stream was aborted before this Component finished rendering.',
         ],
       ];
+      if (componentInfo.key != null) {
+        addValueToProperties('key', componentInfo.key, properties, 0);
+      }
+      if (componentInfo.props != null) {
+        addObjectToProperties(componentInfo.props, properties, 0);
+      }
       performance.measure(entryName, {
         start: startTime < 0 ? 0 : startTime,
         end: childrenEndTime,
@@ -319,6 +214,12 @@ export function logComponentErrored(
           : // eslint-disable-next-line react-internal/safe-string-coercion
             String(error);
       const properties = [['Error', message]];
+      if (componentInfo.key != null) {
+        addValueToProperties('key', componentInfo.key, properties, 0);
+      }
+      if (componentInfo.props != null) {
+        addObjectToProperties(componentInfo.props, properties, 0);
+      }
       performance.measure(entryName, {
         start: startTime < 0 ? 0 : startTime,
         end: childrenEndTime,

filename: packages/react-reconciler/src/ReactFiberPerformanceTrack.js
patch: @@ -26,6 +26,11 @@ import {
   includesOnlyHydrationOrOffscreenLanes,
 } from './ReactFiberLane';
 
+import {
+  addValueToProperties,
+  addObjectToProperties,
+} from 'shared/ReactPerformanceTrackProperties';
+
 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
 
 const supportsUserTiming =
@@ -239,7 +244,7 @@ export function logComponentErrored(
       typeof performance.measure === 'function'
     ) {
       let debugTask: ?ConsoleTask = null;
-      const properties = [];
+      const properties: Array<[string, string]> = [];
       for (let i = 0; i < errors.length; i++) {
         const capturedValue = errors[i];
         if (debugTask == null && capturedValue.source !== null) {
@@ -261,6 +266,12 @@ export function logComponentErrored(
               String(error);
         properties.push(['Error', message]);
       }
+      if (fiber.key !== null) {
+        addValueToProperties('key', fiber.key, properties, 0);
+      }
+      if (fiber.memoizedProps !== null) {
+        addObjectToProperties(fiber.memoizedProps, properties, 0);
+      }
       if (debugTask == null) {
         // If the captured values don't have a debug task, fallback to the
         // error boundary itself.
@@ -320,7 +331,7 @@ function logComponentEffectErrored(
       // $FlowFixMe[method-unbinding]
       typeof performance.measure === 'function'
     ) {
-      const properties = [];
+      const properties: Array<[string, string]> = [];
       for (let i = 0; i < errors.length; i++) {
         const capturedValue = errors[i];
         const error = capturedValue.value;
@@ -334,6 +345,12 @@ function logComponentEffectErrored(
               String(error);
         properties.push(['Error', message]);
       }
+      if (fiber.key !== null) {
+        addValueToProperties('key', fiber.key, properties, 0);
+      }
+      if (fiber.memoizedProps !== null) {
+        addObjectToProperties(fiber.memoizedProps, properties, 0);
+      }
       const options = {
         start: startTime,
         end: endTime,
@@ -804,7 +821,7 @@ export function logRecoveredRenderPhase(
       // $FlowFixMe[method-unbinding]
       typeof performance.measure === 'function'
     ) {
-      const properties = [];
+      const properties: Array<[string, string]> = [];
       for (let i = 0; i < recoverableErrors.length; i++) {
         const capturedValue = recoverableErrors[i];
         const error = capturedValue.value;
@@ -928,7 +945,7 @@ export function logCommitErrored(
       // $FlowFixMe[method-unbinding]
       typeof performance.measure === 'function'
     ) {
-      const properties = [];
+      const properties: Array<[string, string]> = [];
       for (let i = 0; i < errors.length; i++) {
         const capturedValue = errors[i];
         const error = capturedValue.value;

filename: packages/shared/ReactPerformanceTrackProperties.js
patch: @@ -0,0 +1,188 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
+
+import hasOwnProperty from 'shared/hasOwnProperty';
+import isArray from 'shared/isArray';
+import {REACT_ELEMENT_TYPE} from './ReactSymbols';
+import getComponentNameFromType from './getComponentNameFromType';
+
+const EMPTY_ARRAY = 0;
+const COMPLEX_ARRAY = 1;
+const PRIMITIVE_ARRAY = 2; // Primitive values only
+const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
+function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
+  let kind = EMPTY_ARRAY;
+  for (let i = 0; i < array.length; i++) {
+    const value = array[i];
+    if (typeof value === 'object' && value !== null) {
+      if (
+        isArray(value) &&
+        value.length === 2 &&
+        typeof value[0] === 'string'
+      ) {
+        // Key value tuple
+        if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
+          return COMPLEX_ARRAY;
+        }
+        kind = ENTRIES_ARRAY;
+      } else {
+        return COMPLEX_ARRAY;
+      }
+    } else if (typeof value === 'function') {
+      return COMPLEX_ARRAY;
+    } else if (typeof value === 'string' && value.length > 50) {
+      return COMPLEX_ARRAY;
+    } else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
+      return COMPLEX_ARRAY;
+    } else {
+      kind = PRIMITIVE_ARRAY;
+    }
+  }
+  return kind;
+}
+
+export function addObjectToProperties(
+  object: Object,
+  properties: Array<[string, string]>,
+  indent: number,
+): void {
+  for (const key in object) {
+    if (hasOwnProperty.call(object, key) && key[0] !== '_') {
+      const value = object[key];
+      addValueToProperties(key, value, properties, indent);
+    }
+  }
+}
+
+export function addValueToProperties(
+  propertyName: string,
+  value: mixed,
+  properties: Array<[string, string]>,
+  indent: number,
+): void {
+  let desc;
+  switch (typeof value) {
+    case 'object':
+      if (value === null) {
+        desc = 'null';
+        break;
+      } else {
+        if (value.$$typeof === REACT_ELEMENT_TYPE) {
+          // JSX
+          const typeName = getComponentNameFromType(value.type) || '\u2026';
+          const key = value.key;
+          const props: any = value.props;
+          const propsKeys = Object.keys(props);
+          const propsLength = propsKeys.length;
+          if (key == null && propsLength === 0) {
+            desc = '<' + typeName + ' />';
+            break;
+          }
+          if (
+            indent < 3 ||
+            (propsLength === 1 && propsKeys[0] === 'children' && key == null)
+          ) {
+            desc = '<' + typeName + ' \u2026 />';
+            break;
+          }
+          properties.push([
+            '\xa0\xa0'.repeat(indent) + propertyName,
+            '<' + typeName,
+          ]);
+          if (key !== null) {
+            addValueToProperties('key', key, properties, indent + 1);
+          }
+          let hasChildren = false;
+          for (const propKey in props) {
+            if (propKey === 'children') {
+              if (
+                props.children != null &&
+                (!isArray(props.children) || props.children.length > 0)
+              ) {
+                hasChildren = true;
+              }
+            } else if (
+              hasOwnProperty.call(props, propKey) &&
+              propKey[0] !== '_'
+            ) {
+              addValueToProperties(
+                propKey,
+                props[propKey],
+                properties,
+                indent + 1,
+              );
+            }
+          }
+          properties.push([
+            '',
+            hasChildren ? '>\u2026</' + typeName + '>' : '/>',
+          ]);
+          return;
+        }
+        // $FlowFixMe[method-unbinding]
+        const objectToString = Object.prototype.toString.call(value);
+        let objectName = objectToString.slice(8, objectToString.length - 1);
+        if (objectName === 'Array') {
+          const array: Array<any> = (value: any);
+          const kind = getArrayKind(array);
+          if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
+            desc = JSON.stringify(array);
+            break;
+          } else if (kind === ENTRIES_ARRAY) {
+            properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
+            for (let i = 0; i < array.length; i++) {
+              const entry = array[i];
+              addValueToProperties(entry[0], entry[1], properties, indent + 1);
+            }
+            return;
+          }
+        }
+        if (objectName === 'Object') {
+          const proto: any = Object.getPrototypeOf(value);
+          if (proto && typeof proto.constructor === 'function') {
+            objectName = proto.constructor.name;
+          }
+        }
+        properties.push([
+          '\xa0\xa0'.repeat(indent) + propertyName,
+          objectName === 'Object' ? (indent < 3 ? '' : '\u2026') : objectName,
+        ]);
+        if (indent < 3) {
+          addObjectToProperties(value, properties, indent + 1);
+        }
+        return;
+      }
+    case 'function':
+      if (value.name === '') {
+        desc = '() => {}';
+      } else {
+        desc = value.name + '() {}';
+      }
+      break;
+    case 'string':
+      if (value === OMITTED_PROP_ERROR) {
+        desc = '\u2026'; // ellipsis
+      } else {
+        desc = JSON.stringify(value);
+      }
+      break;
+    case 'undefined':
+      desc = 'undefined';
+      break;
+    case 'boolean':
+      desc = value ? 'true' : 'false';
+      break;
+    default:
+      // eslint-disable-next-line react-internal/safe-string-coercion
+      desc = String(value);
+  }
+  properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
+}