[Perf Tracks]: Always log effect that spawned blocking update (#34648)
We've observed some scenarios, where cascading update happens in an effect that was shorter than 0.05ms. In this case, this effect won't be displayed on a timeline, because of the threshold that we are using, but it would be shown in entry properties or in a stack trace. To avoid confusion, we should always log such effects. Validated via manually changing the threshold to 100ms+ and observing that only effects that triggered an update are visible on a timeline.
Ruslan Lesiutin committed
Sep 30, 2025 at 20:05 UTC
063394cf821e5082e834c72ffb9cf6f8575c9b34
2 files changed
+53
-10
packages/react-reconciler/src/ReactFiberCommitWork.js
+33
-10
@@ -130,10 +130,13 @@ import {
130
popComponentEffectDuration,
131
pushComponentEffectErrors,
132
popComponentEffectErrors,
133
+ pushComponentEffectDidSpawnUpdate,
134
+ popComponentEffectDidSpawnUpdate,
135
componentEffectStartTime,
136
componentEffectEndTime,
137
componentEffectDuration,
138
componentEffectErrors,
139
+ componentEffectSpawnedUpdate,
140
} from './ReactProfilerTimer';
141
import {
142
logComponentRender,
@@ -595,6 +598,7 @@ function commitLayoutEffectOnFiber(
598
const prevEffectStart = pushComponentEffectStart();
599
const prevEffectDuration = pushComponentEffectDuration();
600
const prevEffectErrors = pushComponentEffectErrors();
601
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
602
// When updating this function, also update reappearLayoutEffects, which does
603
// most of the same things when an offscreen tree goes from hidden -> visible.
604
const flags = finishedWork.flags;
@@ -876,7 +880,7 @@ function commitLayoutEffectOnFiber(
880
componentEffectStartTime >= 0 &&
881
componentEffectEndTime >= 0
882
) {
879
- if (componentEffectDuration > 0.05) {
883
+ if (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) {
884
logComponentEffect(
885
finishedWork,
886
componentEffectStartTime,
@@ -909,6 +913,7 @@ function commitLayoutEffectOnFiber(
913
popComponentEffectStart(prevEffectStart);
914
popComponentEffectDuration(prevEffectDuration);
915
popComponentEffectErrors(prevEffectErrors);
916
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
917
}
918
919
function abortRootTransitions(
@@ -1430,6 +1435,7 @@ function commitDeletionEffectsOnFiber(
1435
const prevEffectStart = pushComponentEffectStart();
1436
const prevEffectDuration = pushComponentEffectDuration();
1437
const prevEffectErrors = pushComponentEffectErrors();
1438
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
1439
1440
// The cases in this outer switch modify the stack before they traverse
1441
// into their subtree. There are simpler cases in the inner switch
@@ -1750,7 +1756,7 @@ function commitDeletionEffectsOnFiber(
1756
(deletedFiber.mode & ProfileMode) !== NoMode &&
1757
componentEffectStartTime >= 0 &&
1758
componentEffectEndTime >= 0 &&
1753
- componentEffectDuration > 0.05
1759
+ (componentEffectSpawnedUpdate || componentEffectDuration > 0.05)
1760
) {
1761
logComponentEffect(
1762
deletedFiber,
@@ -1764,6 +1770,7 @@ function commitDeletionEffectsOnFiber(
1770
popComponentEffectStart(prevEffectStart);
1771
popComponentEffectDuration(prevEffectDuration);
1772
popComponentEffectErrors(prevEffectErrors);
1773
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
1774
}
1775
1776
function commitSuspenseCallback(finishedWork: Fiber) {
@@ -1987,6 +1994,7 @@ function commitMutationEffectsOnFiber(
1994
const prevEffectStart = pushComponentEffectStart();
1995
const prevEffectDuration = pushComponentEffectDuration();
1996
const prevEffectErrors = pushComponentEffectErrors();
1997
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
1998
const current = finishedWork.alternate;
1999
const flags = finishedWork.flags;
2000
@@ -2611,7 +2619,7 @@ function commitMutationEffectsOnFiber(
2619
componentEffectStartTime >= 0 &&
2620
componentEffectEndTime >= 0
2621
) {
2614
- if (componentEffectDuration > 0.05) {
2622
+ if (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) {
2623
logComponentEffect(
2624
finishedWork,
2625
componentEffectStartTime,
@@ -2644,6 +2652,7 @@ function commitMutationEffectsOnFiber(
2652
popComponentEffectStart(prevEffectStart);
2653
popComponentEffectDuration(prevEffectDuration);
2654
popComponentEffectErrors(prevEffectErrors);
2655
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
2656
}
2657
2658
function commitReconciliationEffects(
@@ -2900,6 +2909,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2909
const prevEffectStart = pushComponentEffectStart();
2910
const prevEffectDuration = pushComponentEffectDuration();
2911
const prevEffectErrors = pushComponentEffectErrors();
2912
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
2913
switch (finishedWork.tag) {
2914
case FunctionComponent:
2915
case ForwardRef:
@@ -2990,7 +3000,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
3000
(finishedWork.mode & ProfileMode) !== NoMode &&
3001
componentEffectStartTime >= 0 &&
3002
componentEffectEndTime >= 0 &&
2993
- componentEffectDuration > 0.05
3003
+ (componentEffectSpawnedUpdate || componentEffectDuration > 0.05)
3004
) {
3005
logComponentEffect(
3006
finishedWork,
@@ -3004,6 +3014,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
3014
popComponentEffectStart(prevEffectStart);
3015
popComponentEffectDuration(prevEffectDuration);
3016
popComponentEffectErrors(prevEffectErrors);
3017
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
3018
}
3019
3020
function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
@@ -3027,6 +3038,7 @@ export function reappearLayoutEffects(
3038
const prevEffectStart = pushComponentEffectStart();
3039
const prevEffectDuration = pushComponentEffectDuration();
3040
const prevEffectErrors = pushComponentEffectErrors();
3041
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
3042
// Turn on layout effects in a tree that previously disappeared.
3043
const flags = finishedWork.flags;
3044
switch (finishedWork.tag) {
@@ -3224,7 +3236,7 @@ export function reappearLayoutEffects(
3236
(finishedWork.mode & ProfileMode) !== NoMode &&
3237
componentEffectStartTime >= 0 &&
3238
componentEffectEndTime >= 0 &&
3227
- componentEffectDuration > 0.05
3239
+ (componentEffectSpawnedUpdate || componentEffectDuration > 0.05)
3240
) {
3241
logComponentEffect(
3242
finishedWork,
@@ -3238,6 +3250,7 @@ export function reappearLayoutEffects(
3250
popComponentEffectStart(prevEffectStart);
3251
popComponentEffectDuration(prevEffectDuration);
3252
popComponentEffectErrors(prevEffectErrors);
3253
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
3254
}
3255
3256
function recursivelyTraverseReappearLayoutEffects(
@@ -3489,6 +3502,7 @@ function commitPassiveMountOnFiber(
3502
const prevEffectStart = pushComponentEffectStart();
3503
const prevEffectDuration = pushComponentEffectDuration();
3504
const prevEffectErrors = pushComponentEffectErrors();
3505
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
3506
const prevDeepEquality = pushDeepEquality();
3507
3508
const isViewTransitionEligible = enableViewTransition
@@ -4060,7 +4074,7 @@ function commitPassiveMountOnFiber(
4074
}
4075
}
4076
if (componentEffectStartTime >= 0 && componentEffectEndTime >= 0) {
4063
- if (componentEffectDuration > 0.05) {
4077
+ if (componentEffectSpawnedUpdate || componentEffectDuration > 0.05) {
4078
logComponentEffect(
4079
finishedWork,
4080
componentEffectStartTime,
@@ -4082,6 +4096,7 @@ function commitPassiveMountOnFiber(
4096
popComponentEffectStart(prevEffectStart);
4097
popComponentEffectDuration(prevEffectDuration);
4098
popComponentEffectErrors(prevEffectErrors);
4099
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
4100
popDeepEquality(prevDeepEquality);
4101
}
4102
@@ -4144,6 +4159,7 @@ export function reconnectPassiveEffects(
4159
const prevEffectStart = pushComponentEffectStart();
4160
const prevEffectDuration = pushComponentEffectDuration();
4161
const prevEffectErrors = pushComponentEffectErrors();
4162
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
4163
const prevDeepEquality = pushDeepEquality();
4164
4165
// If this component rendered in Profiling mode (DEV or in Profiler component) then log its
@@ -4334,7 +4350,7 @@ export function reconnectPassiveEffects(
4350
(finishedWork.mode & ProfileMode) !== NoMode &&
4351
componentEffectStartTime >= 0 &&
4352
componentEffectEndTime >= 0 &&
4337
- componentEffectDuration > 0.05
4353
+ (componentEffectSpawnedUpdate || componentEffectDuration > 0.05)
4354
) {
4355
logComponentEffect(
4356
finishedWork,
@@ -4348,6 +4364,7 @@ export function reconnectPassiveEffects(
4364
popComponentEffectStart(prevEffectStart);
4365
popComponentEffectDuration(prevEffectDuration);
4366
popComponentEffectErrors(prevEffectErrors);
4367
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
4368
popDeepEquality(prevDeepEquality);
4369
}
4370
@@ -4737,6 +4754,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4754
const prevEffectStart = pushComponentEffectStart();
4755
const prevEffectDuration = pushComponentEffectDuration();
4756
const prevEffectErrors = pushComponentEffectErrors();
4757
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
4758
switch (finishedWork.tag) {
4759
case FunctionComponent:
4760
case ForwardRef:
@@ -4833,7 +4851,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4851
(finishedWork.mode & ProfileMode) !== NoMode &&
4852
componentEffectStartTime >= 0 &&
4853
componentEffectEndTime >= 0 &&
4836
- componentEffectDuration > 0.05
4854
+ (componentEffectSpawnedUpdate || componentEffectDuration > 0.05)
4855
) {
4856
logComponentEffect(
4857
finishedWork,
@@ -4846,6 +4864,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
4864
4865
popComponentEffectStart(prevEffectStart);
4866
popComponentEffectDuration(prevEffectDuration);
4867
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
4868
popComponentEffectErrors(prevEffectErrors);
4869
}
4870
@@ -4903,6 +4922,7 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void {
4922
const prevEffectStart = pushComponentEffectStart();
4923
const prevEffectDuration = pushComponentEffectDuration();
4924
const prevEffectErrors = pushComponentEffectErrors();
4925
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
4926
4927
switch (finishedWork.tag) {
4928
case FunctionComponent:
@@ -4942,7 +4962,7 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void {
4962
(finishedWork.mode & ProfileMode) !== NoMode &&
4963
componentEffectStartTime >= 0 &&
4964
componentEffectEndTime >= 0 &&
4945
- componentEffectDuration > 0.05
4965
+ (componentEffectSpawnedUpdate || componentEffectDuration > 0.05)
4966
) {
4967
logComponentEffect(
4968
finishedWork,
@@ -4955,6 +4975,7 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void {
4975
4976
popComponentEffectStart(prevEffectStart);
4977
popComponentEffectDuration(prevEffectDuration);
4978
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
4979
popComponentEffectErrors(prevEffectErrors);
4980
}
4981
@@ -5016,6 +5037,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
5037
const prevEffectStart = pushComponentEffectStart();
5038
const prevEffectDuration = pushComponentEffectDuration();
5039
const prevEffectErrors = pushComponentEffectErrors();
5040
+ const prevEffectDidSpawnUpdate = pushComponentEffectDidSpawnUpdate();
5041
switch (current.tag) {
5042
case FunctionComponent:
5043
case ForwardRef:
@@ -5135,7 +5157,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
5157
(current.mode & ProfileMode) !== NoMode &&
5158
componentEffectStartTime >= 0 &&
5159
componentEffectEndTime >= 0 &&
5138
- componentEffectDuration > 0.05
5160
+ (componentEffectSpawnedUpdate || componentEffectDuration > 0.05)
5161
) {
5162
logComponentEffect(
5163
current,
@@ -5148,6 +5170,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
5170
5171
popComponentEffectStart(prevEffectStart);
5172
popComponentEffectDuration(prevEffectDuration);
5173
+ popComponentEffectDidSpawnUpdate(prevEffectDidSpawnUpdate);
5174
popComponentEffectErrors(prevEffectErrors);
5175
}
5176
packages/react-reconciler/src/ReactProfilerTimer.js
+20
@@ -64,6 +64,7 @@ export let componentEffectDuration: number = -0;
64
export let componentEffectStartTime: number = -1.1;
65
export let componentEffectEndTime: number = -1.1;
66
export let componentEffectErrors: null | Array<CapturedValue<mixed>> = null;
67
+export let componentEffectSpawnedUpdate: boolean = false;
68
69
export let blockingClampTime: number = -0;
70
export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
@@ -153,6 +154,7 @@ export function startUpdateTimerByLane(
154
blockingUpdateComponentName = getComponentNameFromFiber(fiber);
155
}
156
if (isAlreadyRendering()) {
157
+ componentEffectSpawnedUpdate = true;
158
blockingUpdateType = SPAWNED_UPDATE;
159
}
160
const newEventTime = resolveEventTimeStamp();
@@ -495,6 +497,24 @@ export function popComponentEffectErrors(
497
componentEffectErrors = prevErrors;
498
}
499
500
+export function pushComponentEffectDidSpawnUpdate(): boolean {
501
+ if (!enableProfilerTimer || !enableProfilerCommitHooks) {
502
+ return false;
503
+ }
504
+
505
+ const prev = componentEffectSpawnedUpdate;
506
+ componentEffectSpawnedUpdate = false; // Reset.
507
+ return prev;
508
+}
509
+
510
+export function popComponentEffectDidSpawnUpdate(previousValue: boolean): void {
511
+ if (!enableProfilerTimer || !enableProfilerCommitHooks) {
512
+ return;
513
+ }
514
+
515
+ componentEffectSpawnedUpdate = previousValue;
516
+}
517
+
518
/**
519
* Tracks whether the current update was a nested/cascading update (scheduled from a layout effect).
520
*