@samitouri / QOS-React / commits / d2a288febf

Include Component Props in Performance Track (#33655)

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" />

Sebastian Markbåge committed Jun 27, 2025 at 08:45 UTC d2a288febf61a1755b78ce98b3cb17dd412b81e3
5 files changed +245 -139
packages/react-client/src/ReactFlightClient.js
+1 -1
@@ -100,7 +100,7 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
100
101 import {injectInternals} from './ReactFlightClientDevToolsHook';
102
103 -import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
103 +import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
104
105 import ReactVersion from 'shared/ReactVersion';
106
packages/react-client/src/ReactFlightPerformanceTrack.js
+35 -134
@@ -17,10 +17,10 @@ import type {
17
18 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
19
20 -import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
21 -
22 -import hasOwnProperty from 'shared/hasOwnProperty';
23 -import isArray from 'shared/isArray';
20 +import {
21 + addValueToProperties,
22 + addObjectToProperties,
23 +} from 'shared/ReactPerformanceTrackProperties';
24
25 const supportsUserTiming =
26 enableProfilerTimer &&
@@ -33,127 +33,6 @@ const supportsUserTiming =
33 const IO_TRACK = 'Server Requests ⚛';
34 const COMPONENTS_TRACK = 'Server Components ⚛';
35
36 -const EMPTY_ARRAY = 0;
37 -const COMPLEX_ARRAY = 1;
38 -const PRIMITIVE_ARRAY = 2; // Primitive values only
39 -const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
40 -function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
41 - let kind = EMPTY_ARRAY;
42 - for (let i = 0; i < array.length; i++) {
43 - const value = array[i];
44 - if (typeof value === 'object' && value !== null) {
45 - if (
46 - isArray(value) &&
47 - value.length === 2 &&
48 - typeof value[0] === 'string'
49 - ) {
50 - // Key value tuple
51 - if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
52 - return COMPLEX_ARRAY;
53 - }
54 - kind = ENTRIES_ARRAY;
55 - } else {
56 - return COMPLEX_ARRAY;
57 - }
58 - } else if (typeof value === 'function') {
59 - return COMPLEX_ARRAY;
60 - } else if (typeof value === 'string' && value.length > 50) {
61 - return COMPLEX_ARRAY;
62 - } else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
63 - return COMPLEX_ARRAY;
64 - } else {
65 - kind = PRIMITIVE_ARRAY;
66 - }
67 - }
68 - return kind;
69 -}
70 -
71 -function addObjectToProperties(
72 - object: Object,
73 - properties: Array<[string, string]>,
74 - indent: number,
75 -): void {
76 - for (const key in object) {
77 - if (hasOwnProperty.call(object, key) && key[0] !== '_') {
78 - const value = object[key];
79 - addValueToProperties(key, value, properties, indent);
80 - }
81 - }
82 -}
83 -
84 -function addValueToProperties(
85 - propertyName: string,
86 - value: mixed,
87 - properties: Array<[string, string]>,
88 - indent: number,
89 -): void {
90 - let desc;
91 - switch (typeof value) {
92 - case 'object':
93 - if (value === null) {
94 - desc = 'null';
95 - break;
96 - } else {
97 - // $FlowFixMe[method-unbinding]
98 - const objectToString = Object.prototype.toString.call(value);
99 - let objectName = objectToString.slice(8, objectToString.length - 1);
100 - if (objectName === 'Array') {
101 - const array: Array<any> = (value: any);
102 - const kind = getArrayKind(array);
103 - if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
104 - desc = JSON.stringify(array);
105 - break;
106 - } else if (kind === ENTRIES_ARRAY) {
107 - properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
108 - for (let i = 0; i < array.length; i++) {
109 - const entry = array[i];
110 - addValueToProperties(entry[0], entry[1], properties, indent + 1);
111 - }
112 - return;
113 - }
114 - }
115 - if (objectName === 'Object') {
116 - const proto: any = Object.getPrototypeOf(value);
117 - if (proto && typeof proto.constructor === 'function') {
118 - objectName = proto.constructor.name;
119 - }
120 - }
121 - properties.push([
122 - '\xa0\xa0'.repeat(indent) + propertyName,
123 - objectName === 'Object' ? '' : objectName,
124 - ]);
125 - if (indent < 3) {
126 - addObjectToProperties(value, properties, indent + 1);
127 - }
128 - return;
129 - }
130 - case 'function':
131 - if (value.name === '') {
132 - desc = '() => {}';
133 - } else {
134 - desc = value.name + '() {}';
135 - }
136 - break;
137 - case 'string':
138 - if (value === OMITTED_PROP_ERROR) {
139 - desc = '...';
140 - } else {
141 - desc = JSON.stringify(value);
142 - }
143 - break;
144 - case 'undefined':
145 - desc = 'undefined';
146 - break;
147 - case 'boolean':
148 - desc = value ? 'true' : 'false';
149 - break;
150 - default:
151 - // eslint-disable-next-line react-internal/safe-string-coercion
152 - desc = String(value);
153 - }
154 - properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
155 -}
156 -
36 export function markAllTracksInOrder() {
37 if (supportsUserTiming) {
38 // Ensure we create the Server Component track groups earlier than the Client Scheduler
@@ -222,17 +101,27 @@ export function logComponentRender(
101 isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
102 const debugTask = componentInfo.debugTask;
103 if (__DEV__ && debugTask) {
104 + const properties: Array<[string, string]> = [];
105 + if (componentInfo.key != null) {
106 + addValueToProperties('key', componentInfo.key, properties, 0);
107 + }
108 + if (componentInfo.props != null) {
109 + addObjectToProperties(componentInfo.props, properties, 0);
110 + }
111 debugTask.run(
112 // $FlowFixMe[method-unbinding]
227 - console.timeStamp.bind(
228 - console,
229 - entryName,
230 - startTime < 0 ? 0 : startTime,
231 - childrenEndTime,
232 - trackNames[trackIdx],
233 - COMPONENTS_TRACK,
234 - color,
235 - ),
113 + performance.measure.bind(performance, entryName, {
114 + start: startTime < 0 ? 0 : startTime,
115 + end: childrenEndTime,
116 + detail: {
117 + devtools: {
118 + color: color,
119 + track: trackNames[trackIdx],
120 + trackGroup: COMPONENTS_TRACK,
121 + properties,
122 + },
123 + },
124 + }),
125 );
126 } else {
127 console.timeStamp(
@@ -268,6 +157,12 @@ export function logComponentAborted(
157 'The stream was aborted before this Component finished rendering.',
158 ],
159 ];
160 + if (componentInfo.key != null) {
161 + addValueToProperties('key', componentInfo.key, properties, 0);
162 + }
163 + if (componentInfo.props != null) {
164 + addObjectToProperties(componentInfo.props, properties, 0);
165 + }
166 performance.measure(entryName, {
167 start: startTime < 0 ? 0 : startTime,
168 end: childrenEndTime,
@@ -319,6 +214,12 @@ export function logComponentErrored(
214 : // eslint-disable-next-line react-internal/safe-string-coercion
215 String(error);
216 const properties = [['Error', message]];
217 + if (componentInfo.key != null) {
218 + addValueToProperties('key', componentInfo.key, properties, 0);
219 + }
220 + if (componentInfo.props != null) {
221 + addObjectToProperties(componentInfo.props, properties, 0);
222 + }
223 performance.measure(entryName, {
224 start: startTime < 0 ? 0 : startTime,
225 end: childrenEndTime,
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+21 -4
@@ -26,6 +26,11 @@ import {
26 includesOnlyHydrationOrOffscreenLanes,
27 } from './ReactFiberLane';
28
29 +import {
30 + addValueToProperties,
31 + addObjectToProperties,
32 +} from 'shared/ReactPerformanceTrackProperties';
33 +
34 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
35
36 const supportsUserTiming =
@@ -239,7 +244,7 @@ export function logComponentErrored(
244 typeof performance.measure === 'function'
245 ) {
246 let debugTask: ?ConsoleTask = null;
242 - const properties = [];
247 + const properties: Array<[string, string]> = [];
248 for (let i = 0; i < errors.length; i++) {
249 const capturedValue = errors[i];
250 if (debugTask == null && capturedValue.source !== null) {
@@ -261,6 +266,12 @@ export function logComponentErrored(
266 String(error);
267 properties.push(['Error', message]);
268 }
269 + if (fiber.key !== null) {
270 + addValueToProperties('key', fiber.key, properties, 0);
271 + }
272 + if (fiber.memoizedProps !== null) {
273 + addObjectToProperties(fiber.memoizedProps, properties, 0);
274 + }
275 if (debugTask == null) {
276 // If the captured values don't have a debug task, fallback to the
277 // error boundary itself.
@@ -320,7 +331,7 @@ function logComponentEffectErrored(
331 // $FlowFixMe[method-unbinding]
332 typeof performance.measure === 'function'
333 ) {
323 - const properties = [];
334 + const properties: Array<[string, string]> = [];
335 for (let i = 0; i < errors.length; i++) {
336 const capturedValue = errors[i];
337 const error = capturedValue.value;
@@ -334,6 +345,12 @@ function logComponentEffectErrored(
345 String(error);
346 properties.push(['Error', message]);
347 }
348 + if (fiber.key !== null) {
349 + addValueToProperties('key', fiber.key, properties, 0);
350 + }
351 + if (fiber.memoizedProps !== null) {
352 + addObjectToProperties(fiber.memoizedProps, properties, 0);
353 + }
354 const options = {
355 start: startTime,
356 end: endTime,
@@ -804,7 +821,7 @@ export function logRecoveredRenderPhase(
821 // $FlowFixMe[method-unbinding]
822 typeof performance.measure === 'function'
823 ) {
807 - const properties = [];
824 + const properties: Array<[string, string]> = [];
825 for (let i = 0; i < recoverableErrors.length; i++) {
826 const capturedValue = recoverableErrors[i];
827 const error = capturedValue.value;
@@ -928,7 +945,7 @@ export function logCommitErrored(
945 // $FlowFixMe[method-unbinding]
946 typeof performance.measure === 'function'
947 ) {
931 - const properties = [];
948 + const properties: Array<[string, string]> = [];
949 for (let i = 0; i < errors.length; i++) {
950 const capturedValue = errors[i];
951 const error = capturedValue.value;
packages/shared/ReactFlightPropertyAccess.js renamed
packages/shared/ReactPerformanceTrackProperties.js new
+188
@@ -0,0 +1,188 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
11 +
12 +import hasOwnProperty from 'shared/hasOwnProperty';
13 +import isArray from 'shared/isArray';
14 +import {REACT_ELEMENT_TYPE} from './ReactSymbols';
15 +import getComponentNameFromType from './getComponentNameFromType';
16 +
17 +const EMPTY_ARRAY = 0;
18 +const COMPLEX_ARRAY = 1;
19 +const PRIMITIVE_ARRAY = 2; // Primitive values only
20 +const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
21 +function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
22 + let kind = EMPTY_ARRAY;
23 + for (let i = 0; i < array.length; i++) {
24 + const value = array[i];
25 + if (typeof value === 'object' && value !== null) {
26 + if (
27 + isArray(value) &&
28 + value.length === 2 &&
29 + typeof value[0] === 'string'
30 + ) {
31 + // Key value tuple
32 + if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
33 + return COMPLEX_ARRAY;
34 + }
35 + kind = ENTRIES_ARRAY;
36 + } else {
37 + return COMPLEX_ARRAY;
38 + }
39 + } else if (typeof value === 'function') {
40 + return COMPLEX_ARRAY;
41 + } else if (typeof value === 'string' && value.length > 50) {
42 + return COMPLEX_ARRAY;
43 + } else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
44 + return COMPLEX_ARRAY;
45 + } else {
46 + kind = PRIMITIVE_ARRAY;
47 + }
48 + }
49 + return kind;
50 +}
51 +
52 +export function addObjectToProperties(
53 + object: Object,
54 + properties: Array<[string, string]>,
55 + indent: number,
56 +): void {
57 + for (const key in object) {
58 + if (hasOwnProperty.call(object, key) && key[0] !== '_') {
59 + const value = object[key];
60 + addValueToProperties(key, value, properties, indent);
61 + }
62 + }
63 +}
64 +
65 +export function addValueToProperties(
66 + propertyName: string,
67 + value: mixed,
68 + properties: Array<[string, string]>,
69 + indent: number,
70 +): void {
71 + let desc;
72 + switch (typeof value) {
73 + case 'object':
74 + if (value === null) {
75 + desc = 'null';
76 + break;
77 + } else {
78 + if (value.$$typeof === REACT_ELEMENT_TYPE) {
79 + // JSX
80 + const typeName = getComponentNameFromType(value.type) || '\u2026';
81 + const key = value.key;
82 + const props: any = value.props;
83 + const propsKeys = Object.keys(props);
84 + const propsLength = propsKeys.length;
85 + if (key == null && propsLength === 0) {
86 + desc = '<' + typeName + ' />';
87 + break;
88 + }
89 + if (
90 + indent < 3 ||
91 + (propsLength === 1 && propsKeys[0] === 'children' && key == null)
92 + ) {
93 + desc = '<' + typeName + ' \u2026 />';
94 + break;
95 + }
96 + properties.push([
97 + '\xa0\xa0'.repeat(indent) + propertyName,
98 + '<' + typeName,
99 + ]);
100 + if (key !== null) {
101 + addValueToProperties('key', key, properties, indent + 1);
102 + }
103 + let hasChildren = false;
104 + for (const propKey in props) {
105 + if (propKey === 'children') {
106 + if (
107 + props.children != null &&
108 + (!isArray(props.children) || props.children.length > 0)
109 + ) {
110 + hasChildren = true;
111 + }
112 + } else if (
113 + hasOwnProperty.call(props, propKey) &&
114 + propKey[0] !== '_'
115 + ) {
116 + addValueToProperties(
117 + propKey,
118 + props[propKey],
119 + properties,
120 + indent + 1,
121 + );
122 + }
123 + }
124 + properties.push([
125 + '',
126 + hasChildren ? '>\u2026</' + typeName + '>' : '/>',
127 + ]);
128 + return;
129 + }
130 + // $FlowFixMe[method-unbinding]
131 + const objectToString = Object.prototype.toString.call(value);
132 + let objectName = objectToString.slice(8, objectToString.length - 1);
133 + if (objectName === 'Array') {
134 + const array: Array<any> = (value: any);
135 + const kind = getArrayKind(array);
136 + if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
137 + desc = JSON.stringify(array);
138 + break;
139 + } else if (kind === ENTRIES_ARRAY) {
140 + properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
141 + for (let i = 0; i < array.length; i++) {
142 + const entry = array[i];
143 + addValueToProperties(entry[0], entry[1], properties, indent + 1);
144 + }
145 + return;
146 + }
147 + }
148 + if (objectName === 'Object') {
149 + const proto: any = Object.getPrototypeOf(value);
150 + if (proto && typeof proto.constructor === 'function') {
151 + objectName = proto.constructor.name;
152 + }
153 + }
154 + properties.push([
155 + '\xa0\xa0'.repeat(indent) + propertyName,
156 + objectName === 'Object' ? (indent < 3 ? '' : '\u2026') : objectName,
157 + ]);
158 + if (indent < 3) {
159 + addObjectToProperties(value, properties, indent + 1);
160 + }
161 + return;
162 + }
163 + case 'function':
164 + if (value.name === '') {
165 + desc = '() => {}';
166 + } else {
167 + desc = value.name + '() {}';
168 + }
169 + break;
170 + case 'string':
171 + if (value === OMITTED_PROP_ERROR) {
172 + desc = '\u2026'; // ellipsis
173 + } else {
174 + desc = JSON.stringify(value);
175 + }
176 + break;
177 + case 'undefined':
178 + desc = 'undefined';
179 + break;
180 + case 'boolean':
181 + desc = value ? 'true' : 'false';
182 + break;
183 + default:
184 + // eslint-disable-next-line react-internal/safe-string-coercion
185 + desc = String(value);
186 + }
187 + properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
188 +}