Skip to content

Commit 2ca796b

Browse files
committed
Remove recursive calls to renderRoot.
There are a few leftover cases where `renderRoot` is called recursively. All of them are related to synchronously flushing work before its expiration time. We can remove these calls by tracking the last expired level on the root, similar to what we do for other types of pending work, like pings.
1 parent 628d185 commit 2ca796b

File tree

2 files changed

+52
-18
lines changed

2 files changed

+52
-18
lines changed

packages/react-reconciler/src/ReactFiberRoot.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type BaseFiberRootProperties = {|
8484
// The latest time at which a suspended component pinged the root to
8585
// render again
8686
lastPingedTime: ExpirationTime,
87+
lastExpiredTime: ExpirationTime,
8788
|};
8889

8990
// The following attributes are only used by interaction tracing builds.
@@ -132,6 +133,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
132133
this.lastSuspendedTime = NoWork;
133134
this.nextKnownPendingLevel = NoWork;
134135
this.lastPingedTime = NoWork;
136+
this.lastExpiredTime = NoWork;
135137

136138
if (enableSchedulerTracing) {
137139
this.interactionThreadID = unstable_getThreadID();
@@ -192,6 +194,10 @@ export function markRootSuspendedAtTime(
192194
if (expirationTime <= root.lastPingedTime) {
193195
root.lastPingedTime = NoWork;
194196
}
197+
198+
if (expirationTime <= root.lastExpiredTime) {
199+
root.lastExpiredTime = NoWork;
200+
}
195201
}
196202

197203
export function markRootUpdatedAtTime(
@@ -247,4 +253,19 @@ export function markRootFinishedAtTime(
247253
// Clear the pinged time
248254
root.lastPingedTime = NoWork;
249255
}
256+
257+
if (finishedExpirationTime <= root.lastExpiredTime) {
258+
// Clear the expired time
259+
root.lastExpiredTime = NoWork;
260+
}
261+
}
262+
263+
export function markRootExpiredAtTime(
264+
root: FiberRoot,
265+
expirationTime: ExpirationTime,
266+
): void {
267+
const lastExpiredTime = root.lastExpiredTime;
268+
if (lastExpiredTime === NoWork || lastExpiredTime > expirationTime) {
269+
root.lastExpiredTime = expirationTime;
270+
}
250271
}

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
markRootSuspendedAtTime,
6969
markRootFinishedAtTime,
7070
markRootUpdatedAtTime,
71+
markRootExpiredAtTime,
7172
} from './ReactFiberRoot';
7273
import {
7374
NoMode,
@@ -521,10 +522,14 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
521522
// Determines the next expiration time that the root should render, taking
522523
// into account levels that may be suspended, or levels that may have
523524
// received a ping.
524-
//
525+
526+
const lastExpiredTime = root.lastExpiredTime;
527+
if (lastExpiredTime !== NoWork) {
528+
return lastExpiredTime;
529+
}
530+
525531
// "Pending" refers to any update that hasn't committed yet, including if it
526532
// suspended. The "suspended" range is therefore a subset.
527-
528533
const firstPendingTime = root.firstPendingTime;
529534
if (!isRootSuspendedAtTime(root, firstPendingTime)) {
530535
// The highest priority pending time is not suspended. Let's work on that.
@@ -547,6 +552,17 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
547552
// the next level that the root has work on. This function is called on every
548553
// update, and right before exiting a task.
549554
function ensureRootIsScheduled(root: FiberRoot) {
555+
const lastExpiredTime = root.lastExpiredTime;
556+
if (lastExpiredTime !== NoWork) {
557+
// Special case: Expired work should flush synchronously.
558+
root.callbackExpirationTime = Sync;
559+
root.callbackPriority = ImmediatePriority;
560+
root.callbackNode = scheduleSyncCallback(
561+
performSyncWorkOnRoot.bind(null, root, lastExpiredTime),
562+
);
563+
return;
564+
}
565+
550566
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
551567
const existingCallbackNode = root.callbackNode;
552568
if (expirationTime === NoWork) {
@@ -621,20 +637,16 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
621637
// event time. The next update will compute a new event time.
622638
currentEventTime = NoWork;
623639

640+
if (didTimeout) {
641+
// An async update expired.
642+
const currentTime = requestCurrentTime();
643+
markRootExpiredAtTime(root, currentTime);
644+
}
645+
624646
// Determine the next expiration time to work on, using the fields stored
625647
// on the root.
626-
let expirationTime = getNextRootExpirationTimeToWorkOn(root);
648+
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
627649
if (expirationTime !== NoWork) {
628-
if (didTimeout) {
629-
// An async update expired. There may be other expired updates on
630-
// this root.
631-
const currentTime = requestCurrentTime();
632-
if (currentTime < expirationTime) {
633-
// Render all the expired work in a single batch.
634-
expirationTime = currentTime;
635-
}
636-
}
637-
638650
const originalCallbackNode = root.callbackNode;
639651
try {
640652
renderRoot(root, expirationTime, didTimeout);
@@ -673,7 +685,9 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
673685
'means you attempted to commit from inside a lifecycle method.',
674686
);
675687
}
676-
performSyncWorkOnRoot(root, expirationTime);
688+
markRootExpiredAtTime(root, expirationTime);
689+
ensureRootIsScheduled(root);
690+
flushSyncCallbackQueue();
677691
}
678692

679693
export function flushDiscreteUpdates() {
@@ -741,9 +755,8 @@ function flushPendingDiscreteUpdates() {
741755
const roots = rootsWithPendingDiscreteUpdates;
742756
rootsWithPendingDiscreteUpdates = null;
743757
roots.forEach((expirationTime, root) => {
744-
scheduleSyncCallback(
745-
performSyncWorkOnRoot.bind(null, root, expirationTime),
746-
);
758+
markRootExpiredAtTime(root, expirationTime);
759+
ensureRootIsScheduled(root);
747760
});
748761
// Now flush the immediate queue.
749762
flushSyncCallbackQueue();
@@ -1032,7 +1045,7 @@ function renderRoot(
10321045
// synchronously, to see if the error goes away. If there are lower
10331046
// priority updates, let's include those, too, in case they fix the
10341047
// inconsistency. Render at Idle to include all updates.
1035-
performSyncWorkOnRoot(root, Idle);
1048+
markRootExpiredAtTime(root, Idle);
10361049
return;
10371050
}
10381051
// Commit the root in its errored state.

0 commit comments

Comments
 (0)