Skip to content

Commit fdc96e2

Browse files
sebmarkbagetrueadm
authored andcommitted
Don't group Idle/Offscreen work with other work (facebook#17456)
When we suspend we always try a lower level but we shouldn't try offscreen.
1 parent 16b7b57 commit fdc96e2

9 files changed

+137
-5
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

+15-4
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

+114-1
Original file line numberDiff line numberDiff line change
@@ -2222,7 +2222,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
22222222
Scheduler.unstable_runWithPriority(Scheduler.unstable_IdlePriority, () =>
22232223
ReactNoop.render(<Foo renderContent={2} />),
22242224
);
2225-
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A...']);
2225+
// We won't even work on Idle priority.
2226+
expect(Scheduler).toFlushAndYield([]);
22262227

22272228
// We're still suspended.
22282229
expect(ReactNoop.getChildren()).toEqual([]);
@@ -2789,4 +2790,116 @@ describe('ReactSuspenseWithNoopRenderer', () => {
27892790

27902791
expect(root).toMatchRenderedOutput(<span prop="Foo" />);
27912792
});
2793+
2794+
it('should not render hidden content while suspended on higher pri', async () => {
2795+
function Offscreen() {
2796+
Scheduler.unstable_yieldValue('Offscreen');
2797+
return 'Offscreen';
2798+
}
2799+
function App({showContent}) {
2800+
React.useLayoutEffect(() => {
2801+
Scheduler.unstable_yieldValue('Commit');
2802+
});
2803+
return (
2804+
<>
2805+
<div hidden={true}>
2806+
<Offscreen />
2807+
</div>
2808+
<Suspense fallback={<Text text="Loading..." />}>
2809+
{showContent ? <AsyncText text="A" ms={2000} /> : null}
2810+
</Suspense>
2811+
</>
2812+
);
2813+
}
2814+
2815+
// Initial render.
2816+
ReactNoop.render(<App showContent={false} />);
2817+
expect(Scheduler).toFlushAndYieldThrough(['Commit']);
2818+
expect(ReactNoop).toMatchRenderedOutput(<div hidden={true} />);
2819+
2820+
// Start transition.
2821+
React.unstable_withSuspenseConfig(
2822+
() => {
2823+
ReactNoop.render(<App showContent={true} />);
2824+
},
2825+
{timeoutMs: 2000},
2826+
);
2827+
2828+
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2829+
Scheduler.unstable_advanceTime(2000);
2830+
await advanceTimers(2000);
2831+
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2832+
expect(Scheduler).toFlushAndYieldThrough(['A', 'Commit']);
2833+
expect(ReactNoop).toMatchRenderedOutput(
2834+
<>
2835+
<div hidden={true} />
2836+
<span prop="A" />
2837+
</>,
2838+
);
2839+
expect(Scheduler).toFlushAndYield(['Offscreen']);
2840+
expect(ReactNoop).toMatchRenderedOutput(
2841+
<>
2842+
<div hidden={true}>Offscreen</div>
2843+
<span prop="A" />
2844+
</>,
2845+
);
2846+
});
2847+
2848+
it('should be able to unblock higher pri content before suspended hidden', async () => {
2849+
function Offscreen() {
2850+
Scheduler.unstable_yieldValue('Offscreen');
2851+
return 'Offscreen';
2852+
}
2853+
function App({showContent}) {
2854+
React.useLayoutEffect(() => {
2855+
Scheduler.unstable_yieldValue('Commit');
2856+
});
2857+
return (
2858+
<Suspense fallback={<Text text="Loading..." />}>
2859+
<div hidden={true}>
2860+
<AsyncText text="A" ms={2000} />
2861+
<Offscreen />
2862+
</div>
2863+
{showContent ? <AsyncText text="A" ms={2000} /> : null}
2864+
</Suspense>
2865+
);
2866+
}
2867+
2868+
// Initial render.
2869+
ReactNoop.render(<App showContent={false} />);
2870+
expect(Scheduler).toFlushAndYieldThrough(['Commit']);
2871+
expect(ReactNoop).toMatchRenderedOutput(<div hidden={true} />);
2872+
2873+
// Partially render through the hidden content.
2874+
expect(Scheduler).toFlushAndYieldThrough(['Suspend! [A]']);
2875+
2876+
// Start transition.
2877+
React.unstable_withSuspenseConfig(
2878+
() => {
2879+
ReactNoop.render(<App showContent={true} />);
2880+
},
2881+
{timeoutMs: 5000},
2882+
);
2883+
2884+
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2885+
Scheduler.unstable_advanceTime(2000);
2886+
await advanceTimers(2000);
2887+
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2888+
expect(Scheduler).toFlushAndYieldThrough(['A', 'Commit']);
2889+
expect(ReactNoop).toMatchRenderedOutput(
2890+
<>
2891+
<div hidden={true} />
2892+
<span prop="A" />
2893+
</>,
2894+
);
2895+
expect(Scheduler).toFlushAndYield(['A', 'Offscreen']);
2896+
expect(ReactNoop).toMatchRenderedOutput(
2897+
<>
2898+
<div hidden={true}>
2899+
<span prop="A" />Offscreen
2900+
</div>
2901+
<span prop="A" />
2902+
</>,
2903+
);
2904+
});
27922905
});

packages/shared/ReactFeatureFlags.js

+2
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

+1
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

+1
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

+1
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

+1
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

+1
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

+1
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)