Skip to content

Commit 879a684

Browse files
committed
Resume immediately pinged fiber without unwinding
If a fiber suspends, and is pinged immediately in a microtask (or a regular task that fires before React resumes rendering), try rendering the same fiber again without unwinding the stack. This can be super helpful when working with promises and async-await, because even if the outermost promise hasn't been cached before, the underlying data may have been preloaded. In many cases, we can continue rendering immediately without having to show a fallback. This optimization should work during any concurrent (time-sliced) render. It doesn't work during discrete updates because those are semantically required to finish synchronously — those get the current behavior.
1 parent 43e5982 commit 879a684

File tree

6 files changed

+338
-52
lines changed

6 files changed

+338
-52
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Wakeable} from 'shared/ReactTypes';
11+
12+
let suspendedWakeable: Wakeable | null = null;
13+
let wasPinged = false;
14+
let adHocSuspendCount: number = 0;
15+
16+
const MAX_AD_HOC_SUSPEND_COUNT = 50;
17+
18+
export function suspendedWakeableWasPinged() {
19+
return wasPinged;
20+
}
21+
22+
export function trackSuspendedWakeable(wakeable: Wakeable) {
23+
adHocSuspendCount++;
24+
suspendedWakeable = wakeable;
25+
}
26+
27+
export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
28+
if (wakeable === suspendedWakeable) {
29+
// This ping is from the wakeable that just suspended. Mark it as pinged.
30+
// When the work loop resumes, we'll immediately try rendering the fiber
31+
// again instead of unwinding the stack.
32+
wasPinged = true;
33+
return true;
34+
}
35+
return false;
36+
}
37+
38+
export function resetWakeableState() {
39+
suspendedWakeable = null;
40+
wasPinged = false;
41+
adHocSuspendCount = 0;
42+
}
43+
44+
export function throwIfInfinitePingLoopDetected() {
45+
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
46+
// TODO: Guard against an infinite loop by throwing an error if the same
47+
// component suspends too many times in a row. This should be thrown from
48+
// the render phase so that it gets the component stack.
49+
}
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Wakeable} from 'shared/ReactTypes';
11+
12+
let suspendedWakeable: Wakeable | null = null;
13+
let wasPinged = false;
14+
let adHocSuspendCount: number = 0;
15+
16+
const MAX_AD_HOC_SUSPEND_COUNT = 50;
17+
18+
export function suspendedWakeableWasPinged() {
19+
return wasPinged;
20+
}
21+
22+
export function trackSuspendedWakeable(wakeable: Wakeable) {
23+
adHocSuspendCount++;
24+
suspendedWakeable = wakeable;
25+
}
26+
27+
export function attemptToPingSuspendedWakeable(wakeable: Wakeable) {
28+
if (wakeable === suspendedWakeable) {
29+
// This ping is from the wakeable that just suspended. Mark it as pinged.
30+
// When the work loop resumes, we'll immediately try rendering the fiber
31+
// again instead of unwinding the stack.
32+
wasPinged = true;
33+
return true;
34+
}
35+
return false;
36+
}
37+
38+
export function resetWakeableState() {
39+
suspendedWakeable = null;
40+
wasPinged = false;
41+
adHocSuspendCount = 0;
42+
}
43+
44+
export function throwIfInfinitePingLoopDetected() {
45+
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
46+
// TODO: Guard against an infinite loop by throwing an error if the same
47+
// component suspends too many times in a row. This should be thrown from
48+
// the render phase so that it gets the component stack.
49+
}
50+
}

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

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import {
8686
import {
8787
createWorkInProgress,
8888
assignFiberPropertiesInDEV,
89+
resetWorkInProgress,
8990
} from './ReactFiber.new';
9091
import {isRootDehydrated} from './ReactFiberShellHydration';
9192
import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.new';
@@ -245,6 +246,12 @@ import {
245246
isConcurrentActEnvironment,
246247
} from './ReactFiberAct.new';
247248
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
249+
import {
250+
resetWakeableState,
251+
trackSuspendedWakeable,
252+
suspendedWakeableWasPinged,
253+
attemptToPingSuspendedWakeable,
254+
} from './ReactFiberWakeable.new';
248255

249256
const ceil = Math.ceil;
250257

@@ -1549,6 +1556,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
15491556
);
15501557
interruptedWork = interruptedWork.return;
15511558
}
1559+
resetWakeableState();
15521560
}
15531561
workInProgressRoot = root;
15541562
const rootWorkInProgress = createWorkInProgress(root.current, null);
@@ -1884,6 +1892,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
18841892
// If this fiber just suspended, it's possible the data is already
18851893
// cached. Yield to the the main thread to give it a chance to ping. If
18861894
// it does, we can retry immediately without unwinding the stack.
1895+
trackSuspendedWakeable(maybeWakeable);
18871896
break;
18881897
}
18891898
}
@@ -1966,10 +1975,52 @@ function performUnitOfWork(unitOfWork: Fiber): void {
19661975

19671976
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
19681977
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
1969-
// just suspended. It's a separate function to keep the additional logic out
1970-
// of the work loop's hot path.
1978+
// just suspended. In some cases, we may choose to retry the fiber immediately
1979+
// instead of unwinding the stack. It's a separate function to keep the
1980+
// additional logic out of the work loop's hot path.
1981+
1982+
if (!suspendedWakeableWasPinged()) {
1983+
// The wakeable wasn't pinged. Return to the normal work loop. This will
1984+
// unwind the stack, and potentially result in showing a fallback.
1985+
workInProgressIsSuspended = false;
1986+
resetWakeableState();
1987+
completeUnitOfWork(unitOfWork);
1988+
return;
1989+
}
1990+
1991+
// The work-in-progress was immediately pinged. Instead of unwinding the
1992+
// stack and potentially showing a fallback, reset the fiber and try rendering
1993+
// it again.
1994+
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
1995+
1996+
const current = unitOfWork.alternate;
1997+
setCurrentDebugFiberInDEV(unitOfWork);
1998+
1999+
let next;
2000+
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
2001+
startProfilerTimer(unitOfWork);
2002+
next = beginWork(current, unitOfWork, renderLanes);
2003+
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2004+
} else {
2005+
next = beginWork(current, unitOfWork, renderLanes);
2006+
}
2007+
2008+
// The begin phase finished successfully without suspending. Reset the state
2009+
// used to track the fiber while it was suspended. Then return to the normal
2010+
// work loop.
19712011
workInProgressIsSuspended = false;
1972-
completeUnitOfWork(unitOfWork);
2012+
resetWakeableState();
2013+
2014+
resetCurrentDebugFiberInDEV();
2015+
unitOfWork.memoizedProps = unitOfWork.pendingProps;
2016+
if (next === null) {
2017+
// If this doesn't spawn new work, complete the current work.
2018+
completeUnitOfWork(unitOfWork);
2019+
} else {
2020+
workInProgress = next;
2021+
}
2022+
2023+
ReactCurrentOwner.current = null;
19732024
}
19742025

19752026
function completeUnitOfWork(unitOfWork: Fiber): void {
@@ -2783,27 +2834,31 @@ export function pingSuspendedRoot(
27832834
// Received a ping at the same priority level at which we're currently
27842835
// rendering. We might want to restart this render. This should mirror
27852836
// the logic of whether or not a root suspends once it completes.
2786-
2787-
// TODO: If we're rendering sync either due to Sync, Batched or expired,
2788-
// we should probably never restart.
2789-
2790-
// If we're suspended with delay, or if it's a retry, we'll always suspend
2791-
// so we can always restart.
2792-
if (
2793-
workInProgressRootExitStatus === RootSuspendedWithDelay ||
2794-
(workInProgressRootExitStatus === RootSuspended &&
2795-
includesOnlyRetries(workInProgressRootRenderLanes) &&
2796-
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2797-
) {
2798-
// Restart from the root.
2799-
prepareFreshStack(root, NoLanes);
2837+
const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
2838+
if (didPingSuspendedWakeable) {
2839+
// Successfully pinged the in-progress fiber. Don't unwind the stack.
28002840
} else {
2801-
// Even though we can't restart right now, we might get an
2802-
// opportunity later. So we mark this render as having a ping.
2803-
workInProgressRootPingedLanes = mergeLanes(
2804-
workInProgressRootPingedLanes,
2805-
pingedLanes,
2806-
);
2841+
// TODO: If we're rendering sync either due to Sync, Batched or expired,
2842+
// we should probably never restart.
2843+
2844+
// If we're suspended with delay, or if it's a retry, we'll always suspend
2845+
// so we can always restart.
2846+
if (
2847+
workInProgressRootExitStatus === RootSuspendedWithDelay ||
2848+
(workInProgressRootExitStatus === RootSuspended &&
2849+
includesOnlyRetries(workInProgressRootRenderLanes) &&
2850+
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2851+
) {
2852+
// Restart from the root.
2853+
prepareFreshStack(root, NoLanes);
2854+
} else {
2855+
// Even though we can't restart right now, we might get an
2856+
// opportunity later. So we mark this render as having a ping.
2857+
workInProgressRootPingedLanes = mergeLanes(
2858+
workInProgressRootPingedLanes,
2859+
pingedLanes,
2860+
);
2861+
}
28072862
}
28082863
}
28092864

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

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import {
8686
import {
8787
createWorkInProgress,
8888
assignFiberPropertiesInDEV,
89+
resetWorkInProgress,
8990
} from './ReactFiber.old';
9091
import {isRootDehydrated} from './ReactFiberShellHydration';
9192
import {didSuspendOrErrorWhileHydratingDEV} from './ReactFiberHydrationContext.old';
@@ -245,6 +246,12 @@ import {
245246
isConcurrentActEnvironment,
246247
} from './ReactFiberAct.old';
247248
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old';
249+
import {
250+
resetWakeableState,
251+
trackSuspendedWakeable,
252+
suspendedWakeableWasPinged,
253+
attemptToPingSuspendedWakeable,
254+
} from './ReactFiberWakeable.old';
248255

249256
const ceil = Math.ceil;
250257

@@ -1549,6 +1556,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
15491556
);
15501557
interruptedWork = interruptedWork.return;
15511558
}
1559+
resetWakeableState();
15521560
}
15531561
workInProgressRoot = root;
15541562
const rootWorkInProgress = createWorkInProgress(root.current, null);
@@ -1884,6 +1892,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
18841892
// If this fiber just suspended, it's possible the data is already
18851893
// cached. Yield to the the main thread to give it a chance to ping. If
18861894
// it does, we can retry immediately without unwinding the stack.
1895+
trackSuspendedWakeable(maybeWakeable);
18871896
break;
18881897
}
18891898
}
@@ -1966,10 +1975,52 @@ function performUnitOfWork(unitOfWork: Fiber): void {
19661975

19671976
function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void {
19681977
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
1969-
// just suspended. It's a separate function to keep the additional logic out
1970-
// of the work loop's hot path.
1978+
// just suspended. In some cases, we may choose to retry the fiber immediately
1979+
// instead of unwinding the stack. It's a separate function to keep the
1980+
// additional logic out of the work loop's hot path.
1981+
1982+
if (!suspendedWakeableWasPinged()) {
1983+
// The wakeable wasn't pinged. Return to the normal work loop. This will
1984+
// unwind the stack, and potentially result in showing a fallback.
1985+
workInProgressIsSuspended = false;
1986+
resetWakeableState();
1987+
completeUnitOfWork(unitOfWork);
1988+
return;
1989+
}
1990+
1991+
// The work-in-progress was immediately pinged. Instead of unwinding the
1992+
// stack and potentially showing a fallback, reset the fiber and try rendering
1993+
// it again.
1994+
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
1995+
1996+
const current = unitOfWork.alternate;
1997+
setCurrentDebugFiberInDEV(unitOfWork);
1998+
1999+
let next;
2000+
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
2001+
startProfilerTimer(unitOfWork);
2002+
next = beginWork(current, unitOfWork, renderLanes);
2003+
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2004+
} else {
2005+
next = beginWork(current, unitOfWork, renderLanes);
2006+
}
2007+
2008+
// The begin phase finished successfully without suspending. Reset the state
2009+
// used to track the fiber while it was suspended. Then return to the normal
2010+
// work loop.
19712011
workInProgressIsSuspended = false;
1972-
completeUnitOfWork(unitOfWork);
2012+
resetWakeableState();
2013+
2014+
resetCurrentDebugFiberInDEV();
2015+
unitOfWork.memoizedProps = unitOfWork.pendingProps;
2016+
if (next === null) {
2017+
// If this doesn't spawn new work, complete the current work.
2018+
completeUnitOfWork(unitOfWork);
2019+
} else {
2020+
workInProgress = next;
2021+
}
2022+
2023+
ReactCurrentOwner.current = null;
19732024
}
19742025

19752026
function completeUnitOfWork(unitOfWork: Fiber): void {
@@ -2783,27 +2834,31 @@ export function pingSuspendedRoot(
27832834
// Received a ping at the same priority level at which we're currently
27842835
// rendering. We might want to restart this render. This should mirror
27852836
// the logic of whether or not a root suspends once it completes.
2786-
2787-
// TODO: If we're rendering sync either due to Sync, Batched or expired,
2788-
// we should probably never restart.
2789-
2790-
// If we're suspended with delay, or if it's a retry, we'll always suspend
2791-
// so we can always restart.
2792-
if (
2793-
workInProgressRootExitStatus === RootSuspendedWithDelay ||
2794-
(workInProgressRootExitStatus === RootSuspended &&
2795-
includesOnlyRetries(workInProgressRootRenderLanes) &&
2796-
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2797-
) {
2798-
// Restart from the root.
2799-
prepareFreshStack(root, NoLanes);
2837+
const didPingSuspendedWakeable = attemptToPingSuspendedWakeable(wakeable);
2838+
if (didPingSuspendedWakeable) {
2839+
// Successfully pinged the in-progress fiber. Don't unwind the stack.
28002840
} else {
2801-
// Even though we can't restart right now, we might get an
2802-
// opportunity later. So we mark this render as having a ping.
2803-
workInProgressRootPingedLanes = mergeLanes(
2804-
workInProgressRootPingedLanes,
2805-
pingedLanes,
2806-
);
2841+
// TODO: If we're rendering sync either due to Sync, Batched or expired,
2842+
// we should probably never restart.
2843+
2844+
// If we're suspended with delay, or if it's a retry, we'll always suspend
2845+
// so we can always restart.
2846+
if (
2847+
workInProgressRootExitStatus === RootSuspendedWithDelay ||
2848+
(workInProgressRootExitStatus === RootSuspended &&
2849+
includesOnlyRetries(workInProgressRootRenderLanes) &&
2850+
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2851+
) {
2852+
// Restart from the root.
2853+
prepareFreshStack(root, NoLanes);
2854+
} else {
2855+
// Even though we can't restart right now, we might get an
2856+
// opportunity later. So we mark this render as having a ping.
2857+
workInProgressRootPingedLanes = mergeLanes(
2858+
workInProgressRootPingedLanes,
2859+
pingedLanes,
2860+
);
2861+
}
28072862
}
28082863
}
28092864

0 commit comments

Comments
 (0)