Skip to content

Commit 2266225

Browse files
committed
Don't group Idle/Offscreen work with other work
When we suspend we always try a lower level but we shouldn't try offscreen.
1 parent d75323f commit 2266225

9 files changed

+137
-5
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
warnAboutUnmockedScheduler,
2626
flushSuspenseFallbacksInTests,
2727
disableSchedulerTimeoutBasedOnReactExpirationTime,
28+
enableTrainModelFix,
2829
} from 'shared/ReactFeatureFlags';
2930
import ReactSharedInternals from 'shared/ReactSharedInternals';
3031
import invariant from 'shared/invariant';
@@ -539,9 +540,19 @@ function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
539540
// on whichever is higher priority.
540541
const lastPingedTime = root.lastPingedTime;
541542
const nextKnownPendingLevel = root.nextKnownPendingLevel;
542-
return lastPingedTime > nextKnownPendingLevel
543-
? lastPingedTime
544-
: nextKnownPendingLevel;
543+
const nextLevel =
544+
lastPingedTime > nextKnownPendingLevel
545+
? lastPingedTime
546+
: nextKnownPendingLevel;
547+
if (
548+
enableTrainModelFix &&
549+
nextLevel <= Idle &&
550+
firstPendingTime !== nextLevel
551+
) {
552+
// Don't work on Idle/Never priority unless everything else is committed.
553+
return NoWork;
554+
}
555+
return nextLevel;
545556
}
546557

547558
// Use this function to schedule a task for a root. There's only one task per
@@ -2362,7 +2373,7 @@ export function pingSuspendedRoot(
23622373
// Mark the time at which this ping was scheduled.
23632374
root.lastPingedTime = suspendedTime;
23642375

2365-
if (root.finishedExpirationTime === suspendedTime) {
2376+
if (!enableTrainModelFix && root.finishedExpirationTime === suspendedTime) {
23662377
// If there's a pending fallback waiting to commit, throw it away.
23672378
root.finishedExpirationTime = NoWork;
23682379
root.finishedWork = null;

packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2063,7 +2063,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
20632063
Scheduler.unstable_runWithPriority(Scheduler.unstable_IdlePriority, () =>
20642064
ReactNoop.render(<Foo renderContent={2} />),
20652065
);
2066-
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A...']);
2066+
// We won't even work on Idle priority.
2067+
expect(Scheduler).toFlushAndYield([]);
20672068

20682069
// We're still suspended.
20692070
expect(ReactNoop.getChildren()).toEqual([]);
@@ -2630,4 +2631,116 @@ describe('ReactSuspenseWithNoopRenderer', () => {
26302631

26312632
expect(root).toMatchRenderedOutput(<span prop="Foo" />);
26322633
});
2634+
2635+
it('should not render hidden content while suspended on higher pri', async () => {
2636+
function Offscreen() {
2637+
Scheduler.unstable_yieldValue('Offscreen');
2638+
return 'Offscreen';
2639+
}
2640+
function App({showContent}) {
2641+
React.useLayoutEffect(() => {
2642+
Scheduler.unstable_yieldValue('Commit');
2643+
});
2644+
return (
2645+
<>
2646+
<div hidden={true}>
2647+
<Offscreen />
2648+
</div>
2649+
<Suspense fallback={<Text text="Loading..." />}>
2650+
{showContent ? <AsyncText text="A" ms={2000} /> : null}
2651+
</Suspense>
2652+
</>
2653+
);
2654+
}
2655+
2656+
// Initial render.
2657+
ReactNoop.render(<App showContent={false} />);
2658+
expect(Scheduler).toFlushAndYieldThrough(['Commit']);
2659+
expect(ReactNoop).toMatchRenderedOutput(<div hidden={true} />);
2660+
2661+
// Start transition.
2662+
React.unstable_withSuspenseConfig(
2663+
() => {
2664+
ReactNoop.render(<App showContent={true} />);
2665+
},
2666+
{timeoutMs: 2000},
2667+
);
2668+
2669+
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2670+
Scheduler.unstable_advanceTime(2000);
2671+
await advanceTimers(2000);
2672+
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2673+
expect(Scheduler).toFlushAndYieldThrough(['A', 'Commit']);
2674+
expect(ReactNoop).toMatchRenderedOutput(
2675+
<>
2676+
<div hidden={true} />
2677+
<span prop="A" />
2678+
</>,
2679+
);
2680+
expect(Scheduler).toFlushAndYield(['Offscreen']);
2681+
expect(ReactNoop).toMatchRenderedOutput(
2682+
<>
2683+
<div hidden={true}>Offscreen</div>
2684+
<span prop="A" />
2685+
</>,
2686+
);
2687+
});
2688+
2689+
it('should be able to unblock higher pri content before suspended hidden', async () => {
2690+
function Offscreen() {
2691+
Scheduler.unstable_yieldValue('Offscreen');
2692+
return 'Offscreen';
2693+
}
2694+
function App({showContent}) {
2695+
React.useLayoutEffect(() => {
2696+
Scheduler.unstable_yieldValue('Commit');
2697+
});
2698+
return (
2699+
<Suspense fallback={<Text text="Loading..." />}>
2700+
<div hidden={true}>
2701+
<AsyncText text="A" ms={2000} />
2702+
<Offscreen />
2703+
</div>
2704+
{showContent ? <AsyncText text="A" ms={2000} /> : null}
2705+
</Suspense>
2706+
);
2707+
}
2708+
2709+
// Initial render.
2710+
ReactNoop.render(<App showContent={false} />);
2711+
expect(Scheduler).toFlushAndYieldThrough(['Commit']);
2712+
expect(ReactNoop).toMatchRenderedOutput(<div hidden={true} />);
2713+
2714+
// Partially render through the hidden content.
2715+
expect(Scheduler).toFlushAndYieldThrough(['Suspend! [A]']);
2716+
2717+
// Start transition.
2718+
React.unstable_withSuspenseConfig(
2719+
() => {
2720+
ReactNoop.render(<App showContent={true} />);
2721+
},
2722+
{timeoutMs: 5000},
2723+
);
2724+
2725+
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2726+
Scheduler.unstable_advanceTime(2000);
2727+
await advanceTimers(2000);
2728+
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2729+
expect(Scheduler).toFlushAndYieldThrough(['A', 'Commit']);
2730+
expect(ReactNoop).toMatchRenderedOutput(
2731+
<>
2732+
<div hidden={true} />
2733+
<span prop="A" />
2734+
</>,
2735+
);
2736+
expect(Scheduler).toFlushAndYield(['A', 'Offscreen']);
2737+
expect(ReactNoop).toMatchRenderedOutput(
2738+
<>
2739+
<div hidden={true}>
2740+
<span prop="A" />Offscreen
2741+
</div>
2742+
<span prop="A" />
2743+
</>,
2744+
);
2745+
});
26332746
});

packages/shared/ReactFeatureFlags.js

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

8989
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
9090

91+
export const enableTrainModelFix = __EXPERIMENTAL__;
92+
9193
export const enableTrustedTypesIntegration = false;
9294

9395
// Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
4242
export const warnAboutStringRefs = false;
4343
export const disableLegacyContext = false;
4444
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
45+
export const enableTrainModelFix = false;
4546
export const enableTrustedTypesIntegration = false;
4647

4748
// Only used in www builds.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
3636
export const warnAboutStringRefs = false;
3737
export const disableLegacyContext = false;
3838
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
39+
export const enableTrainModelFix = false;
3940
export const enableTrustedTypesIntegration = false;
4041
export const enableNativeTargetAsInstance = false;
4142

packages/shared/forks/ReactFeatureFlags.persistent.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
3636
export const warnAboutStringRefs = false;
3737
export const disableLegacyContext = false;
3838
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
39+
export const enableTrainModelFix = false;
3940
export const enableTrustedTypesIntegration = false;
4041
export const enableNativeTargetAsInstance = false;
4142

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
3636
export const warnAboutStringRefs = false;
3737
export const disableLegacyContext = false;
3838
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
39+
export const enableTrainModelFix = false;
3940
export const enableTrustedTypesIntegration = false;
4041
export const enableNativeTargetAsInstance = false;
4142

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const warnAboutDefaultPropsOnFunctionComponents = false;
3434
export const warnAboutStringRefs = false;
3535
export const disableLegacyContext = false;
3636
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
37+
export const enableTrainModelFix = false;
3738
export const enableTrustedTypesIntegration = false;
3839
export const enableNativeTargetAsInstance = false;
3940

packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const {
1616
disableInputAttributeSyncing,
1717
enableTrustedTypesIntegration,
1818
enableSelectiveHydration,
19+
enableTrainModelFix,
1920
} = require('ReactFeatureFlags');
2021

2122
// In www, we have experimental support for gathering data

0 commit comments

Comments
 (0)