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
+}