Skip to content

Commit fe21c94

Browse files
authored
[Fiber] Yield every other frame for Transition/Retry work (#31828)
This flag first moves the `shouldYield()` logic into React itself. We need this for `postTask` compatibility anyway since this logic is no longer a concern of the scheduler. This means that there can also be no global `requestPaint()` that asks for painting earlier. So this is best rolled out with `enableAlwaysYieldScheduler` (and ideally `enableYieldingBeforePassive`) instead of `enableRequestPaint`. Once in React we can change the yield timing heuristics. This uses the previous 5ms for Idle work to keep everything responsive while doing background work. However, for Transitions and Retries we have seen that same thread animations (like loading states animating, or constant animations like cool Three.js stuff) can take CPU time away from the Transition that causes moving into new content to slow down. Therefore we only yield every 25ms. The purpose of this yield is not to avoid the overhead of yielding, which is very low, but rather to intentionally block any frequently occurring other main thread work like animations from starving our work. If we could we could just tell everyone else to throttle their stuff for ideal scheduling but that's not quite realistic. In other words, the purpose of this is to reduce the frame rate of animations to 30 fps and we achieve this by not yielding. We still do yield to allow the animations to not just stall. This seems like a good balance. The 5ms of Idle is because we don't really need to yield less often since the overhead is low. We keep it low to allow 120 fps animations to run if necessary and our work may not be the only work within a frame so we need to yield early enough to leave enough time left. Similarly we choose 25ms rather than say 35ms to ensure that we push long enough to guarantee to half the frame rate but low enough that there's plenty of time left for a rAF to power each animation every other frame. It's also low enough that if something else interrupts the work like a new interaction, we can still be responsive to that within 50ms or so. We also need to yield in case there's I/O work that needs to get bounced through the main thread. This flag is currently off everywhere since we have so many other scheduling flags but that means there's some urgency to roll those out fully so we can test this one. There's also some tests to update since this doesn't go through the Mock scheduler anymore for yields.
1 parent c8c89fa commit fe21c94

8 files changed

+36
-3
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
enableSiblingPrerendering,
4242
enableComponentPerformanceTrack,
4343
enableYieldingBeforePassive,
44+
enableThrottledScheduling,
4445
} from 'shared/ReactFeatureFlags';
4546
import ReactSharedInternals from 'shared/ReactSharedInternals';
4647
import is from 'shared/objectIs';
@@ -2610,8 +2611,10 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
26102611
// can't trust the result of `shouldYield`, because the host I/O is
26112612
// likely mocked.
26122613
workLoopSync();
2614+
} else if (enableThrottledScheduling) {
2615+
workLoopConcurrent(includesNonIdleWork(lanes));
26132616
} else {
2614-
workLoopConcurrent();
2617+
workLoopConcurrentByScheduler();
26152618
}
26162619
break;
26172620
} catch (thrownValue) {
@@ -2650,10 +2653,27 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
26502653
}
26512654

26522655
/** @noinline */
2653-
function workLoopConcurrent() {
2656+
function workLoopConcurrent(nonIdle: boolean) {
2657+
// We yield every other "frame" when rendering Transition or Retries. Those are blocking
2658+
// revealing new content. The purpose of this yield is not to avoid the overhead of yielding,
2659+
// which is very low, but rather to intentionally block any frequently occuring other main
2660+
// thread work like animations from starving our work. In other words, the purpose of this
2661+
// is to reduce the framerate of animations to 30 frames per second.
2662+
// For Idle work we yield every 5ms to keep animations going smooth.
2663+
if (workInProgress !== null) {
2664+
const yieldAfter = now() + (nonIdle ? 25 : 5);
2665+
do {
2666+
// $FlowFixMe[incompatible-call] flow doesn't know that now() is side-effect free
2667+
performUnitOfWork(workInProgress);
2668+
} while (workInProgress !== null && now() < yieldAfter);
2669+
}
2670+
}
2671+
2672+
/** @noinline */
2673+
function workLoopConcurrentByScheduler() {
26542674
// Perform work until Scheduler asks us to yield
26552675
while (workInProgress !== null && !shouldYield()) {
2656-
// $FlowFixMe[incompatible-call] found when upgrading Flow
2676+
// $FlowFixMe[incompatible-call] flow doesn't know that shouldYield() is side-effect free
26572677
performUnitOfWork(workInProgress);
26582678
}
26592679
}

packages/shared/ReactFeatureFlags.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export const enableLegacyFBSupport = false;
8181
// Fix gated tests that fail with this flag enabled before turning it back on.
8282
export const enableYieldingBeforePassive = false;
8383

84+
// Experiment to intentionally yield less to block high framerate animations.
85+
export const enableThrottledScheduling = false;
86+
8487
export const enableLegacyCache = __EXPERIMENTAL__;
8588

8689
export const enableAsyncIterableChildren = __EXPERIMENTAL__;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const syncLaneExpirationMs = 250;
8181
export const transitionLaneExpirationMs = 5000;
8282
export const enableHydrationLaneScheduling = true;
8383
export const enableYieldingBeforePassive = false;
84+
export const enableThrottledScheduling = false;
8485

8586
// Flow magic to verify the exports of this file match the original version.
8687
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export const enableHydrationLaneScheduling = true;
7373

7474
export const enableYieldingBeforePassive = false;
7575

76+
export const enableThrottledScheduling = false;
77+
7678
// Profiling Only
7779
export const enableProfilerTimer = __PROFILE__;
7880
export const enableProfilerCommitHooks = __PROFILE__;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export const enableUseResourceEffectHook = false;
7272

7373
export const enableYieldingBeforePassive = true;
7474

75+
export const enableThrottledScheduling = false;
76+
7577
// TODO: This must be in sync with the main ReactFeatureFlags file because
7678
// the Test Renderer's value must be the same as the one used by the
7779
// react package.

packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const enableSiblingPrerendering = true;
6969
export const enableUseResourceEffectHook = true;
7070
export const enableHydrationLaneScheduling = true;
7171
export const enableYieldingBeforePassive = false;
72+
export const enableThrottledScheduling = false;
7273

7374
// Flow magic to verify the exports of this file match the original version.
7475
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,7 @@ export const enableHydrationLaneScheduling = true;
8484

8585
export const enableYieldingBeforePassive = false;
8686

87+
export const enableThrottledScheduling = false;
88+
8789
// Flow magic to verify the exports of this file match the original version.
8890
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export const enableLegacyFBSupport = true;
5858

5959
export const enableYieldingBeforePassive = false;
6060

61+
export const enableThrottledScheduling = false;
62+
6163
export const enableHydrationLaneScheduling = true;
6264

6365
export const enableComponentPerformanceTrack = false;

0 commit comments

Comments
 (0)