Skip to content

Commit 0b78161

Browse files
authored
[Fiber] Highlight a Component with Deeply Equal Props in the Performance Track (#33660)
Stacked on #33658 and #33659. If we detect that a component is receiving only deeply equal objects, then we highlight it as potentially problematic and worth looking into. <img width="1055" alt="Screenshot 2025-06-27 at 4 15 28 PM" src="https://github.com/user-attachments/assets/e96c6a05-7fff-4fd7-b59a-36ed79f8e609" /> It's fairly conservative and can bail out for a number of reasons: - We only log it on the first parent that triggered this case since other children could be indirect causes. - If children has changed then we bail out since this component will rerender anyway. This means that it won't warn for a lot of cases that receive plain DOM children since the DOM children won't themselves get logged. - If the component's total render time including children is 100ms or less then we skip warning because rerendering might not be a big deal. - We don't warn if you have shallow equality but could memoize the JSX element itself since we don't typically recommend that and React Compiler doesn't do that. It only warns if you have nested objects too. - If the depth of the objects is deeper than like the 3 levels that we print diffs for then we wouldn't warn since we don't know if they were equal (although we might still warn on a child). - If the component had any updates scheduled on itself (e.g. setState) then we don't warn since it would rerender anyway. This should really consider Context updates too but we don't do that atm. Technically you should still memoize the incoming props even if you also had unrelated updates since it could apply to deeper bailouts.
1 parent dcf83f7 commit 0b78161

File tree

3 files changed

+84
-7
lines changed

3 files changed

+84
-7
lines changed

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ import {
143143
logComponentUnmount,
144144
logComponentReappeared,
145145
logComponentDisappeared,
146+
pushDeepEquality,
147+
popDeepEquality,
146148
} from './ReactFiberPerformanceTrack';
147149
import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
148150
import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue';
@@ -3489,6 +3491,7 @@ function commitPassiveMountOnFiber(
34893491
const prevEffectStart = pushComponentEffectStart();
34903492
const prevEffectDuration = pushComponentEffectDuration();
34913493
const prevEffectErrors = pushComponentEffectErrors();
3494+
const prevDeepEquality = pushDeepEquality();
34923495

34933496
const isViewTransitionEligible = enableViewTransition
34943497
? includesOnlyViewTransitionEligibleLanes(committedLanes)
@@ -3533,6 +3536,7 @@ function commitPassiveMountOnFiber(
35333536
((finishedWork.actualStartTime: any): number),
35343537
endTime,
35353538
inHydratedSubtree,
3539+
committedLanes,
35363540
);
35373541
}
35383542

@@ -3577,6 +3581,7 @@ function commitPassiveMountOnFiber(
35773581
((finishedWork.actualStartTime: any): number),
35783582
endTime,
35793583
inHydratedSubtree,
3584+
committedLanes,
35803585
);
35813586
}
35823587
}
@@ -4079,6 +4084,7 @@ function commitPassiveMountOnFiber(
40794084
popComponentEffectStart(prevEffectStart);
40804085
popComponentEffectDuration(prevEffectDuration);
40814086
popComponentEffectErrors(prevEffectErrors);
4087+
popDeepEquality(prevDeepEquality);
40824088
}
40834089

40844090
function recursivelyTraverseReconnectPassiveEffects(
@@ -4140,6 +4146,8 @@ export function reconnectPassiveEffects(
41404146
const prevEffectStart = pushComponentEffectStart();
41414147
const prevEffectDuration = pushComponentEffectDuration();
41424148
const prevEffectErrors = pushComponentEffectErrors();
4149+
const prevDeepEquality = pushDeepEquality();
4150+
41434151
// If this component rendered in Profiling mode (DEV or in Profiler component) then log its
41444152
// render time. We do this after the fact in the passive effect to avoid the overhead of this
41454153
// getting in the way of the render characteristics and avoid the overhead of unwinding
@@ -4156,6 +4164,7 @@ export function reconnectPassiveEffects(
41564164
((finishedWork.actualStartTime: any): number),
41574165
endTime,
41584166
inHydratedSubtree,
4167+
committedLanes,
41594168
);
41604169
}
41614170

@@ -4340,6 +4349,7 @@ export function reconnectPassiveEffects(
43404349
popComponentEffectStart(prevEffectStart);
43414350
popComponentEffectDuration(prevEffectDuration);
43424351
popComponentEffectErrors(prevEffectErrors);
4352+
popDeepEquality(prevDeepEquality);
43434353
}
43444354

43454355
function recursivelyTraverseAtomicPassiveEffects(
@@ -4389,6 +4399,8 @@ function commitAtomicPassiveEffects(
43894399
committedTransitions: Array<Transition> | null,
43904400
endTime: number, // Profiling-only. The start time of the next Fiber or root completion.
43914401
) {
4402+
const prevDeepEquality = pushDeepEquality();
4403+
43924404
// If this component rendered in Profiling mode (DEV or in Profiler component) then log its
43934405
// render time. A render can happen even if the subtree is offscreen.
43944406
if (
@@ -4403,6 +4415,7 @@ function commitAtomicPassiveEffects(
44034415
((finishedWork.actualStartTime: any): number),
44044416
endTime,
44054417
inHydratedSubtree,
4418+
committedLanes,
44064419
);
44074420
}
44084421

@@ -4453,6 +4466,8 @@ function commitAtomicPassiveEffects(
44534466
break;
44544467
}
44554468
}
4469+
4470+
popDeepEquality(prevDeepEquality);
44564471
}
44574472

44584473
export function commitPassiveUnmountEffects(finishedWork: Fiber): void {

packages/react-reconciler/src/ReactFiberPerformanceTrack.js

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
includesOnlyHydrationLanes,
2525
includesOnlyOffscreenLanes,
2626
includesOnlyHydrationOrOffscreenLanes,
27+
includesSomeLane,
2728
} from './ReactFiberLane';
2829

2930
import {
@@ -104,6 +105,7 @@ function logComponentTrigger(
104105
reusableComponentOptions.start = startTime;
105106
reusableComponentOptions.end = endTime;
106107
reusableComponentDevToolDetails.color = 'warning';
108+
reusableComponentDevToolDetails.tooltipText = trigger;
107109
reusableComponentDevToolDetails.properties = null;
108110
const debugTask = fiber._debugTask;
109111
if (__DEV__ && debugTask) {
@@ -153,11 +155,30 @@ export function logComponentDisappeared(
153155
logComponentTrigger(fiber, startTime, endTime, 'Disconnect');
154156
}
155157

158+
let alreadyWarnedForDeepEquality = false;
159+
160+
export function pushDeepEquality(): boolean {
161+
if (__DEV__) {
162+
// If this is true then we don't reset it to false because we're tracking if any
163+
// parent already warned about having deep equality props in this subtree.
164+
return alreadyWarnedForDeepEquality;
165+
}
166+
return false;
167+
}
168+
169+
export function popDeepEquality(prev: boolean): void {
170+
if (__DEV__) {
171+
alreadyWarnedForDeepEquality = prev;
172+
}
173+
}
174+
156175
const reusableComponentDevToolDetails = {
157176
color: 'primary',
158177
properties: (null: null | Array<[string, string]>),
178+
tooltipText: '',
159179
track: COMPONENTS_TRACK,
160180
};
181+
161182
const reusableComponentOptions = {
162183
start: -0,
163184
end: -0,
@@ -168,11 +189,17 @@ const reusableComponentOptions = {
168189

169190
const resuableChangedPropsEntry = ['Changed Props', ''];
170191

192+
const DEEP_EQUALITY_WARNING =
193+
'This component received deeply equal props. It might benefit from useMemo or the React Compiler in its owner.';
194+
195+
const reusableDeeplyEqualPropsEntry = ['Changed Props', DEEP_EQUALITY_WARNING];
196+
171197
export function logComponentRender(
172198
fiber: Fiber,
173199
startTime: number,
174200
endTime: number,
175201
wasHydrated: boolean,
202+
committedLanes: Lanes,
176203
): void {
177204
const name = getComponentNameFromFiber(fiber);
178205
if (name === null) {
@@ -211,17 +238,36 @@ export function logComponentRender(
211238
) {
212239
// If this is an update, we'll diff the props and emit which ones changed.
213240
const properties: Array<[string, string]> = [resuableChangedPropsEntry];
214-
addObjectDiffToProperties(
241+
const isDeeplyEqual = addObjectDiffToProperties(
215242
alternate.memoizedProps,
216243
props,
217244
properties,
218245
0,
219246
);
220247
if (properties.length > 1) {
248+
if (
249+
isDeeplyEqual &&
250+
!alreadyWarnedForDeepEquality &&
251+
!includesSomeLane(alternate.lanes, committedLanes) &&
252+
(fiber.actualDuration: any) > 100
253+
) {
254+
alreadyWarnedForDeepEquality = true;
255+
// This is the first component in a subtree which rerendered with deeply equal props
256+
// and didn't have its own work scheduled and took a non-trivial amount of time.
257+
// We highlight this for further inspection.
258+
// Note that we only consider this case if properties.length > 1 which it will only
259+
// be if we have emitted any diffs. We'd only emit diffs if there were any nested
260+
// equal objects. Therefore, we don't warn for simple shallow equality.
261+
properties[0] = reusableDeeplyEqualPropsEntry;
262+
reusableComponentDevToolDetails.color = 'warning';
263+
reusableComponentDevToolDetails.tooltipText = DEEP_EQUALITY_WARNING;
264+
} else {
265+
reusableComponentDevToolDetails.color = color;
266+
reusableComponentDevToolDetails.tooltipText = name;
267+
}
268+
reusableComponentDevToolDetails.properties = properties;
221269
reusableComponentOptions.start = startTime;
222270
reusableComponentOptions.end = endTime;
223-
reusableComponentDevToolDetails.color = color;
224-
reusableComponentDevToolDetails.properties = properties;
225271
debugTask.run(
226272
// $FlowFixMe[method-unbinding]
227273
performance.measure.bind(

packages/shared/ReactPerformanceTrackProperties.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,13 @@ export function addValueToProperties(
161161
if (value.status === 'fulfilled') {
162162
// Print the inner value
163163
const idx = properties.length;
164-
addValueToProperties(propertyName, value.value, properties, indent);
164+
addValueToProperties(
165+
propertyName,
166+
value.value,
167+
properties,
168+
indent,
169+
prefix,
170+
);
165171
if (properties.length > idx) {
166172
// Wrap the value or type in Promise descriptor.
167173
const insertedEntry = properties[idx];
@@ -177,6 +183,7 @@ export function addValueToProperties(
177183
value.reason,
178184
properties,
179185
indent,
186+
prefix,
180187
);
181188
if (properties.length > idx) {
182189
// Wrap the value or type in Promise descriptor.
@@ -242,13 +249,15 @@ export function addObjectDiffToProperties(
242249
next: Object,
243250
properties: Array<[string, string]>,
244251
indent: number,
245-
): void {
252+
): boolean {
246253
// Note: We diff even non-owned properties here but things that are shared end up just the same.
247254
// If a property is added or removed, we just emit the property name and omit the value it had.
248255
// Mainly for performance. We need to minimize to only relevant information.
256+
let isDeeplyEqual = true;
249257
for (const key in prev) {
250258
if (!(key in next)) {
251259
properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
260+
isDeeplyEqual = false;
252261
}
253262
}
254263
for (const key in next) {
@@ -262,6 +271,7 @@ export function addObjectDiffToProperties(
262271
// elsewhere but still mark it as a cause of render.
263272
const line = '\xa0\xa0'.repeat(indent) + key;
264273
properties.push([REMOVED + line, '\u2026'], [ADDED + line, '\u2026']);
274+
isDeeplyEqual = false;
265275
continue;
266276
}
267277
if (indent >= 3) {
@@ -286,6 +296,7 @@ export function addObjectDiffToProperties(
286296
const line = '\xa0\xa0'.repeat(indent) + key;
287297
const desc = '<' + typeName + ' \u2026 />';
288298
properties.push([REMOVED + line, desc], [ADDED + line, desc]);
299+
isDeeplyEqual = false;
289300
continue;
290301
}
291302
} else {
@@ -304,13 +315,15 @@ export function addObjectDiffToProperties(
304315
];
305316
properties.push(entry);
306317
const prevLength = properties.length;
307-
addObjectDiffToProperties(
318+
const nestedEqual = addObjectDiffToProperties(
308319
prevValue,
309320
nextValue,
310321
properties,
311322
indent + 1,
312323
);
313-
if (prevLength === properties.length) {
324+
if (!nestedEqual) {
325+
isDeeplyEqual = false;
326+
} else if (prevLength === properties.length) {
314327
// Nothing notably changed inside the nested object. So this is only a change in reference
315328
// equality. Let's note it.
316329
entry[1] =
@@ -349,9 +362,12 @@ export function addObjectDiffToProperties(
349362
// Otherwise, emit the change in property and the values.
350363
addValueToProperties(key, prevValue, properties, indent, REMOVED);
351364
addValueToProperties(key, nextValue, properties, indent, ADDED);
365+
isDeeplyEqual = false;
352366
}
353367
} else {
354368
properties.push([ADDED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
369+
isDeeplyEqual = false;
355370
}
356371
}
372+
return isDeeplyEqual;
357373
}

0 commit comments

Comments
 (0)