@samitouri / QOS-React-2 / commits / e104795f63

[Fiber] Show Diff Render Props in Performance Track in DEV (#33658)

<img width="634" alt="Screenshot 2025-06-27 at 1 13 20 PM" src="https://github.com/user-attachments/assets/dc8c488b-4a23-453f-918f-36b245364934" /> We have to be careful with performance in DEV. It can slow down DX since these are ran whether you're currently running a performance trace or not. It can also show up as misleading since these add time to the "Remaining Effects" entry. I'm not adding all props to the entries. Instead, I'm only adding the changed props after diffing and none for initial mount. I'm trying to as much as possible pick a fast path when possible. I'm also only logging this for the "render" entries and not the effects. If we did something for effects, it would be more like checking with dep changed. This could still have a negative effect on dev performance since we're now also using the slower `performance.measure` API when there's a diff.

Sebastian Markbåge committed Jul 2, 2025 at 16:10 UTC e104795f635e2bb423fe9264b1cfdfff15f551e5
3 files changed +215 -63
packages/react-client/src/ReactFlightPerformanceTrack.js
+10 -10
@@ -103,10 +103,10 @@ export function logComponentRender(
103 if (__DEV__ && debugTask) {
104 const properties: Array<[string, string]> = [];
105 if (componentInfo.key != null) {
106 - addValueToProperties('key', componentInfo.key, properties, 0);
106 + addValueToProperties('key', componentInfo.key, properties, 0, '');
107 }
108 if (componentInfo.props != null) {
109 - addObjectToProperties(componentInfo.props, properties, 0);
109 + addObjectToProperties(componentInfo.props, properties, 0, '');
110 }
111 debugTask.run(
112 // $FlowFixMe[method-unbinding]
@@ -158,10 +158,10 @@ export function logComponentAborted(
158 ],
159 ];
160 if (componentInfo.key != null) {
161 - addValueToProperties('key', componentInfo.key, properties, 0);
161 + addValueToProperties('key', componentInfo.key, properties, 0, '');
162 }
163 if (componentInfo.props != null) {
164 - addObjectToProperties(componentInfo.props, properties, 0);
164 + addObjectToProperties(componentInfo.props, properties, 0, '');
165 }
166 performance.measure(entryName, {
167 start: startTime < 0 ? 0 : startTime,
@@ -215,10 +215,10 @@ export function logComponentErrored(
215 String(error);
216 const properties = [['Error', message]];
217 if (componentInfo.key != null) {
218 - addValueToProperties('key', componentInfo.key, properties, 0);
218 + addValueToProperties('key', componentInfo.key, properties, 0, '');
219 }
220 if (componentInfo.props != null) {
221 - addObjectToProperties(componentInfo.props, properties, 0);
221 + addObjectToProperties(componentInfo.props, properties, 0, '');
222 }
223 performance.measure(entryName, {
224 start: startTime < 0 ? 0 : startTime,
@@ -423,9 +423,9 @@ export function logComponentAwait(
423 if (__DEV__ && debugTask) {
424 const properties: Array<[string, string]> = [];
425 if (typeof value === 'object' && value !== null) {
426 - addObjectToProperties(value, properties, 0);
426 + addObjectToProperties(value, properties, 0, '');
427 } else if (value !== undefined) {
428 - addValueToProperties('Resolved', value, properties, 0);
428 + addValueToProperties('Resolved', value, properties, 0, '');
429 }
430 debugTask.run(
431 // $FlowFixMe[method-unbinding]
@@ -525,9 +525,9 @@ export function logIOInfo(
525 if (__DEV__ && debugTask) {
526 const properties: Array<[string, string]> = [];
527 if (typeof value === 'object' && value !== null) {
528 - addObjectToProperties(value, properties, 0);
528 + addObjectToProperties(value, properties, 0, '');
529 } else if (value !== undefined) {
530 - addValueToProperties('Resolved', value, properties, 0);
530 + addValueToProperties('Resolved', value, properties, 0, '');
531 }
532 debugTask.run(
533 // $FlowFixMe[method-unbinding]
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+62 -45
@@ -29,6 +29,7 @@ import {
29 import {
30 addValueToProperties,
31 addObjectToProperties,
32 + addObjectDiffToProperties,
33 } from 'shared/ReactPerformanceTrackProperties';
34
35 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
@@ -36,26 +37,18 @@ import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
37 const supportsUserTiming =
38 enableProfilerTimer &&
39 typeof console !== 'undefined' &&
39 - typeof console.timeStamp === 'function';
40 + typeof console.timeStamp === 'function' &&
41 + (!__DEV__ ||
42 + // In DEV we also rely on performance.measure
43 + (typeof performance !== 'undefined' &&
44 + // $FlowFixMe[method-unbinding]
45 + typeof performance.measure === 'function'));
46
47 const COMPONENTS_TRACK = 'Components ⚛';
48 const LANES_TRACK_GROUP = 'Scheduler ⚛';
49
50 let currentTrack: string = 'Blocking'; // Lane
51
46 -const reusableLaneDevToolDetails = {
47 - color: 'primary',
48 - track: 'Blocking', // Lane
49 - trackGroup: LANES_TRACK_GROUP,
50 -};
51 -const reusableLaneOptions = {
52 - start: -0,
53 - end: -0,
54 - detail: {
55 - devtools: reusableLaneDevToolDetails,
56 - },
57 -};
58 -
52 export function setCurrentTrackFromLanes(lanes: Lanes): void {
53 currentTrack = getGroupNameOfHighestPriorityLane(lanes);
54 }
@@ -166,6 +159,21 @@ export function logComponentDisappeared(
159 logComponentTrigger(fiber, startTime, endTime, 'Disconnect');
160 }
161
162 +const reusableComponentDevToolDetails = {
163 + color: 'primary',
164 + properties: (null: null | Array<[string, string]>),
165 + track: COMPONENTS_TRACK,
166 +};
167 +const reusableComponentOptions = {
168 + start: -0,
169 + end: -0,
170 + detail: {
171 + devtools: reusableComponentDevToolDetails,
172 + },
173 +};
174 +
175 +const resuableChangedPropsEntry = ['Changed Props', ''];
176 +
177 export function logComponentRender(
178 fiber: Fiber,
179 startTime: number,
@@ -178,8 +186,9 @@ export function logComponentRender(
186 return;
187 }
188 if (supportsUserTiming) {
189 + const alternate = fiber.alternate;
190 let selfTime: number = (fiber.actualDuration: any);
182 - if (fiber.alternate === null || fiber.alternate.child !== fiber.child) {
191 + if (alternate === null || alternate.child !== fiber.child) {
192 for (let child = fiber.child; child !== null; child = child.sibling) {
193 selfTime -= (child.actualDuration: any);
194 }
@@ -200,6 +209,36 @@ export function logComponentRender(
209 : 'error';
210 const debugTask = fiber._debugTask;
211 if (__DEV__ && debugTask) {
212 + const props = fiber.memoizedProps;
213 + if (
214 + props !== null &&
215 + alternate !== null &&
216 + alternate.memoizedProps !== props
217 + ) {
218 + // If this is an update, we'll diff the props and emit which ones changed.
219 + const properties: Array<[string, string]> = [resuableChangedPropsEntry];
220 + addObjectDiffToProperties(
221 + alternate.memoizedProps,
222 + props,
223 + properties,
224 + 0,
225 + );
226 + if (properties.length > 1) {
227 + reusableComponentOptions.start = startTime;
228 + reusableComponentOptions.end = endTime;
229 + reusableComponentDevToolDetails.color = color;
230 + reusableComponentDevToolDetails.properties = properties;
231 + debugTask.run(
232 + // $FlowFixMe[method-unbinding]
233 + performance.measure.bind(
234 + performance,
235 + name,
236 + reusableComponentOptions,
237 + ),
238 + );
239 + return;
240 + }
241 + }
242 debugTask.run(
243 // $FlowFixMe[method-unbinding]
244 console.timeStamp.bind(
@@ -237,12 +276,7 @@ export function logComponentErrored(
276 // Skip
277 return;
278 }
240 - if (
241 - __DEV__ &&
242 - typeof performance !== 'undefined' &&
243 - // $FlowFixMe[method-unbinding]
244 - typeof performance.measure === 'function'
245 - ) {
279 + if (__DEV__) {
280 let debugTask: ?ConsoleTask = null;
281 const properties: Array<[string, string]> = [];
282 for (let i = 0; i < errors.length; i++) {
@@ -267,10 +301,10 @@ export function logComponentErrored(
301 properties.push(['Error', message]);
302 }
303 if (fiber.key !== null) {
270 - addValueToProperties('key', fiber.key, properties, 0);
304 + addValueToProperties('key', fiber.key, properties, 0, '');
305 }
306 if (fiber.memoizedProps !== null) {
273 - addObjectToProperties(fiber.memoizedProps, properties, 0);
307 + addObjectToProperties(fiber.memoizedProps, properties, 0, '');
308 }
309 if (debugTask == null) {
310 // If the captured values don't have a debug task, fallback to the
@@ -325,12 +359,7 @@ function logComponentEffectErrored(
359 // Skip
360 return;
361 }
328 - if (
329 - __DEV__ &&
330 - typeof performance !== 'undefined' &&
331 - // $FlowFixMe[method-unbinding]
332 - typeof performance.measure === 'function'
333 - ) {
362 + if (__DEV__) {
363 const properties: Array<[string, string]> = [];
364 for (let i = 0; i < errors.length; i++) {
365 const capturedValue = errors[i];
@@ -346,10 +375,10 @@ function logComponentEffectErrored(
375 properties.push(['Error', message]);
376 }
377 if (fiber.key !== null) {
349 - addValueToProperties('key', fiber.key, properties, 0);
378 + addValueToProperties('key', fiber.key, properties, 0, '');
379 }
380 if (fiber.memoizedProps !== null) {
352 - addObjectToProperties(fiber.memoizedProps, properties, 0);
381 + addObjectToProperties(fiber.memoizedProps, properties, 0, '');
382 }
383 const options = {
384 start: startTime,
@@ -815,12 +844,7 @@ export function logRecoveredRenderPhase(
844 hydrationFailed: boolean,
845 ): void {
846 if (supportsUserTiming) {
818 - if (
819 - __DEV__ &&
820 - typeof performance !== 'undefined' &&
821 - // $FlowFixMe[method-unbinding]
822 - typeof performance.measure === 'function'
823 - ) {
847 + if (__DEV__) {
848 const properties: Array<[string, string]> = [];
849 for (let i = 0; i < recoverableErrors.length; i++) {
850 const capturedValue = recoverableErrors[i];
@@ -939,12 +963,7 @@ export function logCommitErrored(
963 passive: boolean,
964 ): void {
965 if (supportsUserTiming) {
942 - if (
943 - __DEV__ &&
944 - typeof performance !== 'undefined' &&
945 - // $FlowFixMe[method-unbinding]
946 - typeof performance.measure === 'function'
947 - ) {
966 + if (__DEV__) {
967 const properties: Array<[string, string]> = [];
968 for (let i = 0; i < errors.length; i++) {
969 const capturedValue = errors[i];
@@ -997,8 +1016,6 @@ export function logCommitPhase(
1016 return;
1017 }
1018 if (supportsUserTiming) {
1000 - reusableLaneOptions.start = startTime;
1001 - reusableLaneOptions.end = endTime;
1019 console.timeStamp(
1020 'Commit',
1021 startTime,
packages/shared/ReactPerformanceTrackProperties.js
+143 -8
@@ -53,11 +53,12 @@ export function addObjectToProperties(
53 object: Object,
54 properties: Array<[string, string]>,
55 indent: number,
56 + prefix: string,
57 ): void {
58 for (const key in object) {
59 if (hasOwnProperty.call(object, key) && key[0] !== '_') {
60 const value = object[key];
60 - addValueToProperties(key, value, properties, indent);
61 + addValueToProperties(key, value, properties, indent, prefix);
62 }
63 }
64 }
@@ -67,6 +68,7 @@ export function addValueToProperties(
68 value: mixed,
69 properties: Array<[string, string]>,
70 indent: number,
71 + prefix: string,
72 ): void {
73 let desc;
74 switch (typeof value) {
@@ -94,11 +96,11 @@ export function addValueToProperties(
96 break;
97 }
98 properties.push([
97 - '\xa0\xa0'.repeat(indent) + propertyName,
99 + prefix + '\xa0\xa0'.repeat(indent) + propertyName,
100 '<' + typeName,
101 ]);
102 if (key !== null) {
101 - addValueToProperties('key', key, properties, indent + 1);
103 + addValueToProperties('key', key, properties, indent + 1, prefix);
104 }
105 let hasChildren = false;
106 for (const propKey in props) {
@@ -118,6 +120,7 @@ export function addValueToProperties(
120 props[propKey],
121 properties,
122 indent + 1,
123 + prefix,
124 );
125 }
126 }
@@ -137,10 +140,19 @@ export function addValueToProperties(
140 desc = JSON.stringify(array);
141 break;
142 } else if (kind === ENTRIES_ARRAY) {
140 - properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
143 + properties.push([
144 + prefix + '\xa0\xa0'.repeat(indent) + propertyName,
145 + '',
146 + ]);
147 for (let i = 0; i < array.length; i++) {
148 const entry = array[i];
143 - addValueToProperties(entry[0], entry[1], properties, indent + 1);
149 + addValueToProperties(
150 + entry[0],
151 + entry[1],
152 + properties,
153 + indent + 1,
154 + prefix,
155 + );
156 }
157 return;
158 }
@@ -186,11 +198,11 @@ export function addValueToProperties(
198 }
199 }
200 properties.push([
189 - '\xa0\xa0'.repeat(indent) + propertyName,
201 + prefix + '\xa0\xa0'.repeat(indent) + propertyName,
202 objectName === 'Object' ? (indent < 3 ? '' : '\u2026') : objectName,
203 ]);
204 if (indent < 3) {
193 - addObjectToProperties(value, properties, indent + 1);
205 + addObjectToProperties(value, properties, indent + 1, prefix);
206 }
207 return;
208 }
@@ -218,5 +230,128 @@ export function addValueToProperties(
230 // eslint-disable-next-line react-internal/safe-string-coercion
231 desc = String(value);
232 }
221 - properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
233 + properties.push([prefix + '\xa0\xa0'.repeat(indent) + propertyName, desc]);
234 +}
235 +
236 +const REMOVED = '\u2013\xa0';
237 +const ADDED = '+\xa0';
238 +const UNCHANGED = '\u2007\xa0';
239 +
240 +export function addObjectDiffToProperties(
241 + prev: Object,
242 + next: Object,
243 + properties: Array<[string, string]>,
244 + indent: number,
245 +): void {
246 + // Note: We diff even non-owned properties here but things that are shared end up just the same.
247 + // If a property is added or removed, we just emit the property name and omit the value it had.
248 + // Mainly for performance. We need to minimize to only relevant information.
249 + for (const key in prev) {
250 + if (!(key in next)) {
251 + properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
252 + }
253 + }
254 + for (const key in next) {
255 + if (key in prev) {
256 + const prevValue = prev[key];
257 + const nextValue = next[key];
258 + if (prevValue !== nextValue) {
259 + if (indent === 0 && key === 'children') {
260 + // Omit any change inside the top level children prop since it's expected to change
261 + // with any change to children of the component and their props will be logged
262 + // elsewhere but still mark it as a cause of render.
263 + const line = '\xa0\xa0'.repeat(indent) + key;
264 + properties.push([REMOVED + line, '\u2026'], [ADDED + line, '\u2026']);
265 + continue;
266 + }
267 + if (indent >= 3) {
268 + // Just fallthrough to print the two values if we're deep.
269 + // This will skip nested properties of the objects.
270 + } else if (
271 + typeof prevValue === 'object' &&
272 + typeof nextValue === 'object' &&
273 + prevValue !== null &&
274 + nextValue !== null &&
275 + prevValue.$$typeof === nextValue.$$typeof
276 + ) {
277 + if (nextValue.$$typeof === REACT_ELEMENT_TYPE) {
278 + if (
279 + prevValue.type === nextValue.type &&
280 + prevValue.key === nextValue.key
281 + ) {
282 + // If the only thing that has changed is the props of a nested element, then
283 + // we omit the props because it is likely to be represented as a diff elsewhere.
284 + const typeName =
285 + getComponentNameFromType(nextValue.type) || '\u2026';
286 + const line = '\xa0\xa0'.repeat(indent) + key;
287 + const desc = '<' + typeName + ' \u2026 />';
288 + properties.push([REMOVED + line, desc], [ADDED + line, desc]);
289 + continue;
290 + }
291 + } else {
292 + // $FlowFixMe[method-unbinding]
293 + const prevKind = Object.prototype.toString.call(prevValue);
294 + // $FlowFixMe[method-unbinding]
295 + const nextKind = Object.prototype.toString.call(nextValue);
296 + if (
297 + prevKind === nextKind &&
298 + (nextKind === '[object Object]' || nextKind === '[object Array]')
299 + ) {
300 + // Diff nested object
301 + const entry = [
302 + UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
303 + nextKind === '[object Array]' ? 'Array' : '',
304 + ];
305 + properties.push(entry);
306 + const prevLength = properties.length;
307 + addObjectDiffToProperties(
308 + prevValue,
309 + nextValue,
310 + properties,
311 + indent + 1,
312 + );
313 + if (prevLength === properties.length) {
314 + // Nothing notably changed inside the nested object. So this is only a change in reference
315 + // equality. Let's note it.
316 + entry[1] =
317 + 'Referentially unequal but deeply equal objects. Consider memoization.';
318 + }
319 + continue;
320 + }
321 + }
322 + } else if (
323 + typeof prevValue === 'function' &&
324 + typeof nextValue === 'function' &&
325 + prevValue.name === nextValue.name &&
326 + prevValue.length === nextValue.length
327 + ) {
328 + // $FlowFixMe[method-unbinding]
329 + const prevSrc = Function.prototype.toString.call(prevValue);
330 + // $FlowFixMe[method-unbinding]
331 + const nextSrc = Function.prototype.toString.call(nextValue);
332 + if (prevSrc === nextSrc) {
333 + // This looks like it might be the same function but different closures.
334 + let desc;
335 + if (nextValue.name === '') {
336 + desc = '() => {}';
337 + } else {
338 + desc = nextValue.name + '() {}';
339 + }
340 + properties.push([
341 + UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
342 + desc +
343 + ' Referentially unequal function closure. Consider memoization.',
344 + ]);
345 + continue;
346 + }
347 + }
348 +
349 + // Otherwise, emit the change in property and the values.
350 + addValueToProperties(key, prevValue, properties, indent, REMOVED);
351 + addValueToProperties(key, nextValue, properties, indent, ADDED);
352 + }
353 + } else {
354 + properties.push([ADDED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
355 + }
356 + }
357 }