Skip to content

Commit

Permalink
Move update processing into microtask
Browse files Browse the repository at this point in the history
When React receives new input (via `setState`, a Suspense promise
resolution, and so on), it needs to ensure there's a rendering task
associated with the update. Most of this happens
`ensureRootIsScheduled`.

If a single event contains multiple updates, we end up running the
scheduling code once per update. But this is wasteful because we really
only need to run it once, at the end of the event (or in the case of
flushSync, at the end of the scope function's execution).

So this PR moves the scheduling logic to happen in a microtask instead.
In some cases, we will force it run earlier than that, like for
`flushSync`, but since updates are batched by default, it will almost
always happen in the microtask. Even for discrete updates.

In production, this should have no observable behavior difference. In
a testing environment that uses `act`, this should also not have a
behavior difference because React will push these tasks to an internal
`act` queue.

However, tests that do not use `act` and do not simulate an actual
production environment (like an e2e test) may be affected. For example,
before this change, if a test were to call `setState` outside of `act`
and then immediately call `jest.runAllTimers()`, the update would be
synchronously applied. After this change, that will no longer work
because the rendering task (a timer, in this case) isn't scheduled until
after the microtask queue has run.

I don't expect this to be an issue in practice because most people do
not write their tests this way. They either use `act`, or they write
e2e-style tests.

The biggest exception has been... our own internal test suite. Until
recently, many of our tests were written in a way that accidentally
relied on the updates being scheduled synchronously. Over the past few
weeks, @tyao1 and I have gradually converted the test suite to use a new
set of testing helpers that are resilient to this implementation detail.

(There are also some old Relay tests that were written in the style of
React's internal test suite. Those will need to be fixed, too.)

The larger motivation behind this change, aside from a minor performance
improvement, is we intend to use this new microtask to perform
additional logic that doesn't yet exist. Like inferring the priority
of a custom event.
  • Loading branch information
acdlite committed Mar 30, 2023
1 parent c4a075c commit f8cd4bb
Show file tree
Hide file tree
Showing 10 changed files with 59 additions and 386 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ describe('ProfilingCache', () => {
2 => 0,
},
"passiveEffectDuration": null,
"priorityLevel": "Immediate",
"priorityLevel": "Normal",
"timestamp": 0,
"updaters": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,9 +803,9 @@ describe('ReactDOMServerSelectiveHydration', () => {
// boundary. See the next test below.
assertLog([
'D',
'Clicked D',
'B', // Ideally this should be later.
'C',
'Clicked D',
'Hover C',
'A',
]);
Expand Down Expand Up @@ -963,10 +963,10 @@ describe('ReactDOMServerSelectiveHydration', () => {
// boundary. See the next test below.
assertLog([
'D',
'Clicked D',
'B', // Ideally this should be later.
'C',
// Capture phase isn't replayed
'Clicked D',
// Mouseout isn't replayed
'Mouse Over C',
'Mouse Enter C',
Expand Down
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function FiberRootNode(
this.cancelPendingCommit = null;
this.context = null;
this.pendingContext = null;
this.next = null;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.eventTimes = createLaneMap(NoLanes);
Expand Down
38 changes: 11 additions & 27 deletions packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import type {FiberRoot} from './ReactInternalTypes';
import type {Lane} from './ReactFiberLane';
import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities';

import {enableDeferRootSchedulingToMicrotask} from 'shared/ReactFeatureFlags';
import {
NoLane,
NoLanes,
SyncLane,
getHighestPriorityLane,
getNextLanes,
includesOnlyNonUrgentLanes,
Expand Down Expand Up @@ -111,14 +111,6 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
scheduleImmediateTask(processRootScheduleInMicrotask);
}
}

if (!enableDeferRootSchedulingToMicrotask) {
// While this flag is disabled, we schedule the task immediately instead
// of waiting for the microtask to fire.
// TODO: We need to land enableDeferRootSchedulingToMicrotask ASAP to
// unblock additional features we have planned.
scheduleTaskForRootDuringMicrotask(root, now());
}
}

// TODO: Rename to something else. This isn't a generic callback queue anymore.
Expand Down Expand Up @@ -255,7 +247,7 @@ function processRootScheduleInMicrotask() {
} else {
// This root still has work. Keep it in the list.
prev = root;
if (includesSyncLane) {
if (includesSyncLane(nextLanes)) {
mightHavePendingSyncWork = true;
}
}
Expand All @@ -275,9 +267,6 @@ function scheduleTaskForRootDuringMicrotask(
// rendering task right before we yield to the main thread. It should never be
// called synchronously.
//
// TODO: Unless enableDeferRootSchedulingToMicrotask is off. We need to land
// that ASAP to unblock additional features we have planned.
//
// This function also never performs React work synchronously; it should
// only schedule work to be performed later, in a separate task or microtask.

Expand Down Expand Up @@ -306,30 +295,26 @@ function scheduleTaskForRootDuringMicrotask(
) {
// Fast path: There's nothing to work on.
if (existingCallbackNode !== null) {
if (__DEV__ && existingCallbackNode === fakeActCallbackNode) {
// Special `act` case: check if this is the fake callback node used by
// the `act` implementation.
} else {
Scheduler_cancelCallback(existingCallbackNode);
}
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return NoLane;
}

// We use the highest priority lane to represent the priority of the callback.
const existingCallbackPriority = root.callbackPriority;
const newCallbackPriority = getHighestPriorityLane(nextLanes);

// Schedule a new callback in the host environment.
if (includesSyncLane(newCallbackPriority)) {
if (includesSyncLane(nextLanes)) {
// Synchronous work will be flushed at the end of the microtask; we don't
// need to schedule anything extra.
mightHavePendingSyncWork = true;
root.callbackPriority = newCallbackPriority;
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {
// We use the highest priority lane to represent the priority of the callback.
const existingCallbackPriority = root.callbackPriority;
const newCallbackPriority = getHighestPriorityLane(nextLanes);

if (
newCallbackPriority === existingCallbackPriority &&
// Special case related to `act`. If the currently scheduled task is a
Expand Down Expand Up @@ -374,9 +359,8 @@ function scheduleTaskForRootDuringMicrotask(

root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;
}

return newCallbackPriority;
}

export type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null;
Expand Down
118 changes: 0 additions & 118 deletions packages/react-reconciler/src/ReactFiberSyncTaskQueue.js

This file was deleted.

Loading

0 comments on commit f8cd4bb

Please sign in to comment.