Skip to content

Commit d6913dd

Browse files
committed
[WIP] Schedule prerender after something suspends
Part of a series of PRs related to sibling prerendering. I have the implementation working locally, but still working on splitting it up into a reasonable sequence of steps so we can land it incrementally. There's likely to be some regressions due to the scope of the change, so I've also done my best to keep the change behind a feature flag. Opening this in draft mode while I continue to work on updating the test suite, which requires many changes. --- Adds the concept of a "prerender". These special renders are spawned whenever something suspends (and we're not already prerendering). The purpose is to move speculative rendering work into a separate phase that does not block the UI from updating. For example, during a transition, if something suspends, we should not speculatively prerender siblings that will be replaced by a fallback in the UI until *after* the fallback has been shown to the user.
1 parent f72bdce commit d6913dd

10 files changed

+483
-59
lines changed

packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,15 @@ describe('ReactCache', () => {
148148
error = e;
149149
}
150150
expect(error.message).toMatch('Failed to load: Hi');
151-
assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
151+
assertLog([
152+
'Promise rejected [Hi]',
153+
'Error! [Hi]',
154+
'Error! [Hi]',
155+
156+
...(gate('enableSiblingPrerendering')
157+
? ['Error! [Hi]', 'Error! [Hi]']
158+
: []),
159+
]);
152160

153161
// Should throw again on a subsequent read
154162
root.render(<App />);
@@ -191,6 +199,7 @@ describe('ReactCache', () => {
191199
}
192200
});
193201

202+
// @gate enableSiblingPrerendering
194203
it('evicts least recently used values', async () => {
195204
ReactCache.unstable_setGlobalCacheLimit(3);
196205

@@ -206,15 +215,13 @@ describe('ReactCache', () => {
206215
await waitForAll(['Suspend! [1]', 'Loading...']);
207216
jest.advanceTimersByTime(100);
208217
assertLog(['Promise resolved [1]']);
209-
await waitForAll([1, 'Suspend! [2]']);
218+
await waitForAll([1, 'Suspend! [2]', 1, 'Suspend! [2]', 'Suspend! [3]']);
210219

211220
jest.advanceTimersByTime(100);
212-
assertLog(['Promise resolved [2]']);
213-
await waitForAll([1, 2, 'Suspend! [3]']);
221+
assertLog(['Promise resolved [2]', 'Promise resolved [3]']);
222+
await waitForAll([1, 2, 3]);
214223

215224
await act(() => jest.advanceTimersByTime(100));
216-
assertLog(['Promise resolved [3]', 1, 2, 3]);
217-
218225
expect(root).toMatchRenderedOutput('123');
219226

220227
// Render 1, 4, 5
@@ -234,6 +241,9 @@ describe('ReactCache', () => {
234241
1,
235242
4,
236243
'Suspend! [5]',
244+
1,
245+
4,
246+
'Suspend! [5]',
237247
'Promise resolved [5]',
238248
1,
239249
4,
@@ -267,6 +277,9 @@ describe('ReactCache', () => {
267277
1,
268278
2,
269279
'Suspend! [3]',
280+
1,
281+
2,
282+
'Suspend! [3]',
270283
'Promise resolved [3]',
271284
1,
272285
2,

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,28 +229,49 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
229229

230230
const suspendedLanes = root.suspendedLanes;
231231
const pingedLanes = root.pingedLanes;
232+
const warmLanes = root.warmLanes;
232233

233234
// Do not work on any idle work until all the non-idle work has finished,
234235
// even if the work is suspended.
235236
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
236237
if (nonIdlePendingLanes !== NoLanes) {
238+
// First check for fresh updates.
237239
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
238240
if (nonIdleUnblockedLanes !== NoLanes) {
239241
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
240242
} else {
243+
// No fresh updates. Check if suspended work has been pinged.
241244
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
242245
if (nonIdlePingedLanes !== NoLanes) {
243246
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
247+
} else {
248+
// Nothing has been pinged. Check for lanes that need to be prewarmed.
249+
const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
250+
if (lanesToPrewarm !== NoLanes) {
251+
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
252+
}
244253
}
245254
}
246255
} else {
247256
// The only remaining work is Idle.
257+
// TODO: Idle isn't really used anywhere, and the thinking around
258+
// speculative rendering has evolved since this was implemented. Consider
259+
// removing until we've thought about this again.
260+
261+
// First check for fresh updates.
248262
const unblockedLanes = pendingLanes & ~suspendedLanes;
249263
if (unblockedLanes !== NoLanes) {
250264
nextLanes = getHighestPriorityLanes(unblockedLanes);
251265
} else {
266+
// No fresh updates. Check if suspended work has been pinged.
252267
if (pingedLanes !== NoLanes) {
253268
nextLanes = getHighestPriorityLanes(pingedLanes);
269+
} else {
270+
// Nothing has been pinged. Check for lanes that need to be prewarmed.
271+
const lanesToPrewarm = pendingLanes & ~warmLanes;
272+
if (lanesToPrewarm !== NoLanes) {
273+
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
274+
}
254275
}
255276
}
256277
}
@@ -335,6 +356,21 @@ export function getNextLanesToFlushSync(
335356
return NoLanes;
336357
}
337358

359+
export function checkIfRootIsPrerendering(
360+
root: FiberRoot,
361+
renderLanes: Lanes,
362+
): boolean {
363+
const pendingLanes = root.pendingLanes;
364+
const suspendedLanes = root.suspendedLanes;
365+
const pingedLanes = root.pingedLanes;
366+
// Remove lanes that are suspended (but not pinged)
367+
const unblockedLanes = pendingLanes & ~(suspendedLanes & ~pingedLanes);
368+
369+
// If there are no unsuspended or pinged lanes, that implies that we're
370+
// performing a prerender.
371+
return unblockedLanes === 0;
372+
}
373+
338374
export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
339375
let entangledLanes = renderLanes;
340376

@@ -670,17 +706,27 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
670706
if (updateLane !== IdleLane) {
671707
root.suspendedLanes = NoLanes;
672708
root.pingedLanes = NoLanes;
709+
root.warmLanes = NoLanes;
673710
}
674711
}
675712

676713
export function markRootSuspended(
677714
root: FiberRoot,
678715
suspendedLanes: Lanes,
679716
spawnedLane: Lane,
717+
didSkipSuspendedSiblings: boolean,
680718
) {
681719
root.suspendedLanes |= suspendedLanes;
682720
root.pingedLanes &= ~suspendedLanes;
683721

722+
if (!didSkipSuspendedSiblings) {
723+
// Mark these lanes as warm so we know there's nothing else to work on.
724+
root.warmLanes |= suspendedLanes;
725+
} else {
726+
// Render unwound without attempting all the siblings. Do no mark the lanes
727+
// as warm. This will cause a prewarm render to be scheduled.
728+
}
729+
684730
// The suspended lanes are no longer CPU-bound. Clear their expiration times.
685731
const expirationTimes = root.expirationTimes;
686732
let lanes = suspendedLanes;
@@ -700,6 +746,9 @@ export function markRootSuspended(
700746

701747
export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
702748
root.pingedLanes |= root.suspendedLanes & pingedLanes;
749+
// The data that just resolved could have unblocked additional children, which
750+
// will also need to be prewarmed if something suspends again.
751+
root.warmLanes &= ~pingedLanes;
703752
}
704753

705754
export function markRootFinished(
@@ -714,6 +763,7 @@ export function markRootFinished(
714763
// Let's try everything again
715764
root.suspendedLanes = NoLanes;
716765
root.pingedLanes = NoLanes;
766+
root.warmLanes = NoLanes;
717767

718768
root.expiredLanes &= remainingLanes;
719769

packages/react-reconciler/src/ReactFiberRoot.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function FiberRootNode(
7575
this.pendingLanes = NoLanes;
7676
this.suspendedLanes = NoLanes;
7777
this.pingedLanes = NoLanes;
78+
this.warmLanes = NoLanes;
7879
this.expiredLanes = NoLanes;
7980
this.finishedLanes = NoLanes;
8081
this.errorRecoveryDisabledLanes = NoLanes;

0 commit comments

Comments
 (0)