mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Fiber] Mark cascading updates (#31866)
A common source of performance problems is due to cascading renders from calling `setState` in `useLayoutEffect` or `useEffect`. This marks the entry from the update to when we start the render as red and `"Cascade"` to highlight this. <img width="964" alt="Screenshot 2024-12-19 at 10 54 59 PM" src="https://github.com/user-attachments/assets/2bfa91e6-1dc1-4b7f-a659-50aaf2a97e83" /> In addition to this case, there's another case where you call `setState` multiple times in the same event causing multiple renders. This might be due to multiple `flushSync`, or spawned a microtasks from a `useLayoutEffect`. In theory it could also be from a microtask scheduled after the first `setState`. This one we can only detect if it's from an event that has a `window.event` since otherwise it's hard to know if we're still in the same event. <img width="1210" alt="Screenshot 2024-12-19 at 11 38 44 PM" src="https://github.com/user-attachments/assets/ee188bc4-8ebb-4e95-b5a5-4d724856c27d" /> I decided against making a ping in a microtask considered a cascade. Because that should ideally be using the Suspense Optimization and so wouldn't be considered multi-pass. <img width="1284" alt="Screenshot 2024-12-19 at 11 07 30 PM" src="https://github.com/user-attachments/assets/2d173750-a475-41a0-b6cf-679d15c4ca97" /> We might consider making the whole render phase and maybe commit phase red but that should maybe reserved for actual errors. The "Blocked" phase really represents the `setState` and so will have the stack trace of the first update.
This commit is contained in:
+13
-6
@@ -276,11 +276,15 @@ export function logBlockingStart(
|
||||
eventTime: number,
|
||||
eventType: null | string,
|
||||
eventIsRepeat: boolean,
|
||||
isSpawnedUpdate: boolean,
|
||||
renderStartTime: number,
|
||||
lanes: Lanes,
|
||||
): void {
|
||||
if (supportsUserTiming) {
|
||||
reusableLaneDevToolDetails.track = 'Blocking';
|
||||
// If a blocking update was spawned within render or an effect, that's considered a cascading render.
|
||||
// If you have a second blocking update within the same event, that suggests multiple flushSync or
|
||||
// setState in a microtask which is also considered a cascade.
|
||||
if (eventTime > 0 && eventType !== null) {
|
||||
// Log the time from the event timeStamp until we called setState.
|
||||
reusableLaneDevToolDetails.color = eventIsRepeat
|
||||
@@ -295,14 +299,17 @@ export function logBlockingStart(
|
||||
}
|
||||
if (updateTime > 0) {
|
||||
// Log the time from when we called setState until we started rendering.
|
||||
reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes(
|
||||
lanes,
|
||||
)
|
||||
? 'tertiary-light'
|
||||
: 'primary-light';
|
||||
reusableLaneDevToolDetails.color = isSpawnedUpdate
|
||||
? 'error'
|
||||
: includesOnlyHydrationOrOffscreenLanes(lanes)
|
||||
? 'tertiary-light'
|
||||
: 'primary-light';
|
||||
reusableLaneOptions.start = updateTime;
|
||||
reusableLaneOptions.end = renderStartTime;
|
||||
performance.measure('Blocked', reusableLaneOptions);
|
||||
performance.measure(
|
||||
isSpawnedUpdate ? 'Cascade' : 'Blocked',
|
||||
reusableLaneOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -128,12 +128,12 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
|
||||
// We're inside an `act` scope.
|
||||
if (!didScheduleMicrotask_act) {
|
||||
didScheduleMicrotask_act = true;
|
||||
scheduleImmediateTask(processRootScheduleInMicrotask);
|
||||
scheduleImmediateRootScheduleTask();
|
||||
}
|
||||
} else {
|
||||
if (!didScheduleMicrotask) {
|
||||
didScheduleMicrotask = true;
|
||||
scheduleImmediateTask(processRootScheduleInMicrotask);
|
||||
scheduleImmediateRootScheduleTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,13 +229,17 @@ function flushSyncWorkAcrossRoots_impl(
|
||||
isFlushingWork = false;
|
||||
}
|
||||
|
||||
function processRootScheduleInMicrotask() {
|
||||
function processRootScheduleInImmediateTask() {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
// Track the currently executing event if there is one so we can ignore this
|
||||
// event when logging events.
|
||||
trackSchedulerEvent();
|
||||
}
|
||||
|
||||
processRootScheduleInMicrotask();
|
||||
}
|
||||
|
||||
function processRootScheduleInMicrotask() {
|
||||
// This function is always called inside a microtask. It should never be
|
||||
// called synchronously.
|
||||
didScheduleMicrotask = false;
|
||||
@@ -558,7 +562,7 @@ function cancelCallback(callbackNode: mixed) {
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleImmediateTask(cb: () => mixed) {
|
||||
function scheduleImmediateRootScheduleTask() {
|
||||
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
|
||||
// Special case: Inside an `act` scope, we push microtasks to the fake `act`
|
||||
// callback queue. This is because we currently support calling `act`
|
||||
@@ -566,7 +570,7 @@ function scheduleImmediateTask(cb: () => mixed) {
|
||||
// that you always await the result so that the microtasks have a chance to
|
||||
// run. But it hasn't happened yet.
|
||||
ReactSharedInternals.actQueue.push(() => {
|
||||
cb();
|
||||
processRootScheduleInMicrotask();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
@@ -588,14 +592,20 @@ function scheduleImmediateTask(cb: () => mixed) {
|
||||
// wrong semantically but it prevents an infinite loop. The bug is
|
||||
// Safari's, not ours, so we just do our best to not crash even though
|
||||
// the behavior isn't completely correct.
|
||||
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
|
||||
Scheduler_scheduleCallback(
|
||||
ImmediateSchedulerPriority,
|
||||
processRootScheduleInImmediateTask,
|
||||
);
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
processRootScheduleInMicrotask();
|
||||
});
|
||||
} else {
|
||||
// If microtasks are not supported, use Scheduler.
|
||||
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
|
||||
Scheduler_scheduleCallback(
|
||||
ImmediateSchedulerPriority,
|
||||
processRootScheduleInImmediateTask,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -236,6 +236,7 @@ import {
|
||||
blockingEventTime,
|
||||
blockingEventType,
|
||||
blockingEventIsRepeat,
|
||||
blockingSpawnedUpdate,
|
||||
blockingSuspendedTime,
|
||||
transitionClampTime,
|
||||
transitionStartTime,
|
||||
@@ -1664,11 +1665,8 @@ export function flushSyncWork(): boolean {
|
||||
|
||||
export function isAlreadyRendering(): boolean {
|
||||
// Used by the renderer to print a warning if certain APIs are called from
|
||||
// the wrong context.
|
||||
return (
|
||||
__DEV__ &&
|
||||
(executionContext & (RenderContext | CommitContext)) !== NoContext
|
||||
);
|
||||
// the wrong context, and for profiling warnings.
|
||||
return (executionContext & (RenderContext | CommitContext)) !== NoContext;
|
||||
}
|
||||
|
||||
export function isInvalidExecutionContextForEventFunction(): boolean {
|
||||
@@ -1797,6 +1795,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
|
||||
clampedEventTime,
|
||||
blockingEventType,
|
||||
blockingEventIsRepeat,
|
||||
blockingSpawnedUpdate,
|
||||
renderStartTime,
|
||||
lanes,
|
||||
);
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
enableComponentPerformanceTrack,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {isAlreadyRendering} from './ReactFiberWorkLoop';
|
||||
|
||||
// Intentionally not named imports because Rollup would use dynamic dispatch for
|
||||
// CommonJS interop named imports.
|
||||
import * as Scheduler from 'scheduler';
|
||||
@@ -50,6 +52,7 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
|
||||
export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState.
|
||||
export let blockingEventType: null | string = null; // Event type of the first setState.
|
||||
export let blockingEventIsRepeat: boolean = false;
|
||||
export let blockingSpawnedUpdate: boolean = false;
|
||||
export let blockingSuspendedTime: number = -1.1;
|
||||
// TODO: This should really be one per Transition lane.
|
||||
export let transitionClampTime: number = -0;
|
||||
@@ -78,6 +81,9 @@ export function startUpdateTimerByLane(lane: Lane): void {
|
||||
if (isSyncLane(lane) || isBlockingLane(lane)) {
|
||||
if (blockingUpdateTime < 0) {
|
||||
blockingUpdateTime = now();
|
||||
if (isAlreadyRendering()) {
|
||||
blockingSpawnedUpdate = true;
|
||||
}
|
||||
const newEventTime = resolveEventTimeStamp();
|
||||
const newEventType = resolveEventType();
|
||||
if (
|
||||
@@ -85,6 +91,11 @@ export function startUpdateTimerByLane(lane: Lane): void {
|
||||
newEventType !== blockingEventType
|
||||
) {
|
||||
blockingEventIsRepeat = false;
|
||||
} else if (newEventType !== null) {
|
||||
// If this is a second update in the same event, we treat it as a spawned update.
|
||||
// This might be a microtask spawned from useEffect, multiple flushSync or
|
||||
// a setState in a microtask spawned after the first setState. Regardless it's bad.
|
||||
blockingSpawnedUpdate = true;
|
||||
}
|
||||
blockingEventTime = newEventTime;
|
||||
blockingEventType = newEventType;
|
||||
@@ -141,6 +152,7 @@ export function clearBlockingTimers(): void {
|
||||
blockingUpdateTime = -1.1;
|
||||
blockingSuspendedTime = -1.1;
|
||||
blockingEventIsRepeat = true;
|
||||
blockingSpawnedUpdate = false;
|
||||
}
|
||||
|
||||
export function startAsyncTransitionTimer(): void {
|
||||
|
||||
Reference in New Issue
Block a user