Skip to content

Commit 01c6228

Browse files
author
Brian Vaughn
committed
Add "nested-update" phase to Profiler API
Background: State updates that are scheduled in a layout effect (useLayoutEffect or componentDidMount / componentDidUpdate) get processed synchronously by React before it yields to the browser to paint. This is done so that components can adjust their layout (e.g. position and size a tooltip) without any visible shifting being seen by users. This type of update is often called a "nested update" or a "cascading update". Because they delay paint, nested updates are considered expensive and should be avoided when possible. For example, effects that do not impact layout (e.g. adding event handlers, logging impressions) can be safely deferred to the passive effect phase by using useEffect instead. This PR updates the Profiler API to explicitly flag nested updates so they can be monitored for and avoided when possible. Implementation: I considered a few approaches for this. Add a new callback (e.g. onNestedUpdateScheduled) to the Profiler that gets called when a nested updates gets scheduled. Add an additional boolean parameter to the end of existing callbacks (e.g. wasNestedUpdate). Update the phase param to add an additional variant: "mount", "update", or "nested-update" (new). I think the third option makes for the best API so that's what I've implemented in this PR. Because the Profiler API is stable, this change will need to remain behind a feature flag until v18. I've turned the feature flag on for Facebook builds though after confirming that Web Speed does not currently make use of the phase parameter. Quirks: One quirk about the implementation I've chosen is that errors thrown during the layout phase are also reported as nested updates. I believe this is appropriate since these errors get processed synchronously and block paint. Errors thrown during render or from within passive effects are not affected by this change.
1 parent 6b28eb6 commit 01c6228

16 files changed

+173
-30
lines changed

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
enableSchedulerTracing,
3232
enableProfilerTimer,
3333
enableProfilerCommitHooks,
34+
enableProfilerNestedUpdatePhase,
3435
enableSuspenseServerRenderer,
3536
enableFundamentalAPI,
3637
enableSuspenseCallback,
@@ -94,6 +95,7 @@ import {
9495
import {onCommitUnmount} from './ReactFiberDevToolsHook.new';
9596
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
9697
import {
98+
isCurrentUpdateNested,
9799
getCommitTime,
98100
recordLayoutEffectDuration,
99101
startLayoutEffectTimer,
@@ -369,22 +371,24 @@ function commitProfilerPassiveEffect(
369371
// It does not get reset until the start of the next commit phase.
370372
const commitTime = getCommitTime();
371373

374+
let phase = finishedWork.alternate === null ? 'mount' : 'update';
375+
if (enableProfilerNestedUpdatePhase) {
376+
if (isCurrentUpdateNested()) {
377+
phase = 'nested-update';
378+
}
379+
}
380+
372381
if (typeof onPostCommit === 'function') {
373382
if (enableSchedulerTracing) {
374383
onPostCommit(
375384
id,
376-
finishedWork.alternate === null ? 'mount' : 'update',
385+
phase,
377386
passiveEffectDuration,
378387
commitTime,
379388
finishedRoot.memoizedInteractions,
380389
);
381390
} else {
382-
onPostCommit(
383-
id,
384-
finishedWork.alternate === null ? 'mount' : 'update',
385-
passiveEffectDuration,
386-
commitTime,
387-
);
391+
onPostCommit(id, phase, passiveEffectDuration, commitTime);
388392
}
389393
}
390394
break;
@@ -1362,11 +1366,18 @@ function commitLayoutEffectsForProfiler(
13621366
const OnRenderFlag = Update;
13631367
const OnCommitFlag = Callback;
13641368

1369+
let phase = current === null ? 'mount' : 'update';
1370+
if (enableProfilerNestedUpdatePhase) {
1371+
if (isCurrentUpdateNested()) {
1372+
phase = 'nested-update';
1373+
}
1374+
}
1375+
13651376
if ((flags & OnRenderFlag) !== NoFlags && typeof onRender === 'function') {
13661377
if (enableSchedulerTracing) {
13671378
onRender(
13681379
finishedWork.memoizedProps.id,
1369-
current === null ? 'mount' : 'update',
1380+
phase,
13701381
finishedWork.actualDuration,
13711382
finishedWork.treeBaseDuration,
13721383
finishedWork.actualStartTime,
@@ -1376,7 +1387,7 @@ function commitLayoutEffectsForProfiler(
13761387
} else {
13771388
onRender(
13781389
finishedWork.memoizedProps.id,
1379-
current === null ? 'mount' : 'update',
1390+
phase,
13801391
finishedWork.actualDuration,
13811392
finishedWork.treeBaseDuration,
13821393
finishedWork.actualStartTime,
@@ -1393,15 +1404,15 @@ function commitLayoutEffectsForProfiler(
13931404
if (enableSchedulerTracing) {
13941405
onCommit(
13951406
finishedWork.memoizedProps.id,
1396-
current === null ? 'mount' : 'update',
1407+
phase,
13971408
effectDuration,
13981409
commitTime,
13991410
finishedRoot.memoizedInteractions,
14001411
);
14011412
} else {
14021413
onCommit(
14031414
finishedWork.memoizedProps.id,
1404-
current === null ? 'mount' : 'update',
1415+
phase,
14051416
effectDuration,
14061417
commitTime,
14071418
);

packages/react-reconciler/src/ReactFiberCommitWork.old.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
enableSchedulerTracing,
3131
enableProfilerTimer,
3232
enableProfilerCommitHooks,
33+
enableProfilerNestedUpdatePhase,
3334
enableSuspenseServerRenderer,
3435
enableFundamentalAPI,
3536
enableSuspenseCallback,
@@ -73,6 +74,7 @@ import invariant from 'shared/invariant';
7374
import {onCommitUnmount} from './ReactFiberDevToolsHook.old';
7475
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
7576
import {
77+
isCurrentUpdateNested,
7678
getCommitTime,
7779
recordLayoutEffectDuration,
7880
startLayoutEffectTimer,
@@ -434,22 +436,24 @@ export function commitPassiveEffectDurations(
434436
// It does not get reset until the start of the next commit phase.
435437
const commitTime = getCommitTime();
436438

439+
let phase = finishedWork.alternate === null ? 'mount' : 'update';
440+
if (enableProfilerNestedUpdatePhase) {
441+
if (isCurrentUpdateNested()) {
442+
phase = 'nested-update';
443+
}
444+
}
445+
437446
if (typeof onPostCommit === 'function') {
438447
if (enableSchedulerTracing) {
439448
onPostCommit(
440449
id,
441-
finishedWork.alternate === null ? 'mount' : 'update',
450+
phase,
442451
passiveEffectDuration,
443452
commitTime,
444453
finishedRoot.memoizedInteractions,
445454
);
446455
} else {
447-
onPostCommit(
448-
id,
449-
finishedWork.alternate === null ? 'mount' : 'update',
450-
passiveEffectDuration,
451-
commitTime,
452-
);
456+
onPostCommit(id, phase, passiveEffectDuration, commitTime);
453457
}
454458
}
455459

@@ -706,11 +710,18 @@ function commitLifeCycles(
706710

707711
const commitTime = getCommitTime();
708712

713+
let phase = current === null ? 'mount' : 'update';
714+
if (enableProfilerNestedUpdatePhase) {
715+
if (isCurrentUpdateNested()) {
716+
phase = 'nested-update';
717+
}
718+
}
719+
709720
if (typeof onRender === 'function') {
710721
if (enableSchedulerTracing) {
711722
onRender(
712723
finishedWork.memoizedProps.id,
713-
current === null ? 'mount' : 'update',
724+
phase,
714725
finishedWork.actualDuration,
715726
finishedWork.treeBaseDuration,
716727
finishedWork.actualStartTime,
@@ -720,7 +731,7 @@ function commitLifeCycles(
720731
} else {
721732
onRender(
722733
finishedWork.memoizedProps.id,
723-
current === null ? 'mount' : 'update',
734+
phase,
724735
finishedWork.actualDuration,
725736
finishedWork.treeBaseDuration,
726737
finishedWork.actualStartTime,
@@ -734,15 +745,15 @@ function commitLifeCycles(
734745
if (enableSchedulerTracing) {
735746
onCommit(
736747
finishedWork.memoizedProps.id,
737-
current === null ? 'mount' : 'update',
748+
phase,
738749
effectDuration,
739750
commitTime,
740751
finishedRoot.memoizedInteractions,
741752
);
742753
} else {
743754
onCommit(
744755
finishedWork.memoizedProps.id,
745-
current === null ? 'mount' : 'update',
756+
phase,
746757
effectDuration,
747758
commitTime,
748759
);

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
enableSuspenseServerRenderer,
2222
replayFailedUnitOfWorkWithInvokeGuardedCallback,
2323
enableProfilerTimer,
24+
enableProfilerNestedUpdatePhase,
2425
enableSchedulerTracing,
2526
warnAboutUnmockedScheduler,
2627
deferRenderPhaseUpdateToNextBatch,
@@ -195,9 +196,11 @@ import {
195196
} from './ReactFiberStack.new';
196197

197198
import {
199+
markNestedUpdateScheduled,
198200
recordCommitTime,
199201
startProfilerTimer,
200202
stopProfilerTimerIfRunningAndRecordDelta,
203+
syncNestedUpdateFlag,
201204
} from './ReactProfilerTimer.new';
202205

203206
// DEV stuff
@@ -939,6 +942,10 @@ function markRootSuspended(root, suspendedLanes) {
939942
// This is the entry point for synchronous tasks that don't go
940943
// through Scheduler
941944
function performSyncWorkOnRoot(root) {
945+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
946+
syncNestedUpdateFlag();
947+
}
948+
942949
invariant(
943950
(executionContext & (RenderContext | CommitContext)) === NoContext,
944951
'Should not already be working.',
@@ -1996,6 +2003,10 @@ function commitRootImpl(root, renderPriorityLevel) {
19962003
}
19972004

19982005
if (remainingLanes === SyncLane) {
2006+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
2007+
markNestedUpdateScheduled();
2008+
}
2009+
19992010
// Count the number of times the root synchronously re-renders without
20002011
// finishing. If there are too many, it indicates an infinite update loop.
20012012
if (root === rootWithNestedUpdates) {

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
replayFailedUnitOfWorkWithInvokeGuardedCallback,
2323
enableProfilerTimer,
2424
enableProfilerCommitHooks,
25+
enableProfilerNestedUpdatePhase,
2526
enableSchedulerTracing,
2627
warnAboutUnmockedScheduler,
2728
deferRenderPhaseUpdateToNextBatch,
@@ -207,11 +208,13 @@ import {
207208
} from './ReactFiberStack.old';
208209

209210
import {
211+
markNestedUpdateScheduled,
210212
recordCommitTime,
211213
recordPassiveEffectDuration,
212214
startPassiveEffectTimer,
213215
startProfilerTimer,
214216
stopProfilerTimerIfRunningAndRecordDelta,
217+
syncNestedUpdateFlag,
215218
} from './ReactProfilerTimer.old';
216219

217220
// DEV stuff
@@ -962,6 +965,10 @@ function markRootSuspended(root, suspendedLanes) {
962965
// This is the entry point for synchronous tasks that don't go
963966
// through Scheduler
964967
function performSyncWorkOnRoot(root) {
968+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
969+
syncNestedUpdateFlag();
970+
}
971+
965972
invariant(
966973
(executionContext & (RenderContext | CommitContext)) === NoContext,
967974
'Should not already be working.',
@@ -2189,6 +2196,10 @@ function commitRootImpl(root, renderPriorityLevel) {
21892196
}
21902197

21912198
if (remainingLanes === SyncLane) {
2199+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
2200+
markNestedUpdateScheduled();
2201+
}
2202+
21922203
// Count the number of times the root synchronously re-renders without
21932204
// finishing. If there are too many, it indicates an infinite update loop.
21942205
if (root === rootWithNestedUpdates) {

packages/react-reconciler/src/ReactProfilerTimer.new.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import type {Fiber} from './ReactInternalTypes';
1111

1212
import {
13-
enableProfilerTimer,
1413
enableProfilerCommitHooks,
14+
enableProfilerNestedUpdatePhase,
15+
enableProfilerTimer,
1516
} from 'shared/ReactFeatureFlags';
1617
import {Profiler} from './ReactWorkTags';
1718

@@ -23,10 +24,13 @@ const {unstable_now: now} = Scheduler;
2324

2425
export type ProfilerTimer = {
2526
getCommitTime(): number,
27+
isCurrentUpdateNested(): boolean,
28+
markNestedUpdateScheduled(): void,
2629
recordCommitTime(): void,
2730
startProfilerTimer(fiber: Fiber): void,
2831
stopProfilerTimerIfRunning(fiber: Fiber): void,
2932
stopProfilerTimerIfRunningAndRecordDelta(fiber: Fiber): void,
33+
syncNestedUpdateFlag(): void,
3034
...
3135
};
3236

@@ -35,6 +39,42 @@ let layoutEffectStartTime: number = -1;
3539
let profilerStartTime: number = -1;
3640
let passiveEffectStartTime: number = -1;
3741

42+
/**
43+
* Tracks whether the current update was a nested/cascading update (scheduled from a layout effect).
44+
*
45+
* The overall sequence is:
46+
* 1. render
47+
* 2. commit (and call `onRender`, `onCommit`)
48+
* 3. check for nested updates
49+
* 4. flush passive effects (and call `onPostCommit`)
50+
*
51+
* Nested updates are identified in step 3 above,
52+
* but step 4 still applies to the work that was just committed.
53+
* We use two flags to track nested updates then:
54+
* one tracks whether the upcoming update is a nested update,
55+
* and the other tracks whether the current update was a nested update.
56+
* The first value gets synced to the second at the start of the render phase.
57+
*/
58+
let currentUpdateIsNested: boolean = false;
59+
let nestedUpdateScheduled: boolean = false;
60+
61+
function isCurrentUpdateNested(): boolean {
62+
return currentUpdateIsNested;
63+
}
64+
65+
function markNestedUpdateScheduled(): void {
66+
if (enableProfilerNestedUpdatePhase) {
67+
nestedUpdateScheduled = true;
68+
}
69+
}
70+
71+
function syncNestedUpdateFlag(): void {
72+
if (enableProfilerNestedUpdatePhase) {
73+
currentUpdateIsNested = nestedUpdateScheduled;
74+
nestedUpdateScheduled = false;
75+
}
76+
}
77+
3878
function getCommitTime(): number {
3979
return commitTime;
4080
}
@@ -161,6 +201,8 @@ function transferActualDuration(fiber: Fiber): void {
161201

162202
export {
163203
getCommitTime,
204+
isCurrentUpdateNested,
205+
markNestedUpdateScheduled,
164206
recordCommitTime,
165207
recordLayoutEffectDuration,
166208
recordPassiveEffectDuration,
@@ -169,5 +211,6 @@ export {
169211
startProfilerTimer,
170212
stopProfilerTimerIfRunning,
171213
stopProfilerTimerIfRunningAndRecordDelta,
214+
syncNestedUpdateFlag,
172215
transferActualDuration,
173216
};

0 commit comments

Comments
 (0)