Skip to content

Commit a8b3623

Browse files
committed
Prerender during same pass if blocked anyway
If something suspends in the shell — i.e. we won't replace the suspended content with a fallback — we might as well prerender the siblings during the current render pass, instead of spawning a separate prerender pass. This is implemented by setting the "is prerendering" flag to true whenever we suspend in the shell. But only if we haven't already skipped over some siblings, because if so, then we need to schedule a separate prerender pass regardless.
1 parent e10e868 commit a8b3623

20 files changed

+96
-521
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,14 @@ describe('ReactCache', () => {
238238
await act(() => jest.advanceTimersByTime(100));
239239
assertLog([
240240
'Promise resolved [4]',
241+
241242
1,
242243
4,
243244
'Suspend! [5]',
244245
1,
245246
4,
246247
'Suspend! [5]',
248+
247249
'Promise resolved [5]',
248250
1,
249251
4,
@@ -274,12 +276,14 @@ describe('ReactCache', () => {
274276
await act(() => jest.advanceTimersByTime(100));
275277
assertLog([
276278
'Promise resolved [2]',
279+
277280
1,
278281
2,
279282
'Suspend! [3]',
280283
1,
281284
2,
282285
'Suspend! [3]',
286+
283287
'Promise resolved [3]',
284288
1,
285289
2,

packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
744744
// Because it suspended, it remains on the current path
745745
expect(div.textContent).toBe('/path/a');
746746
});
747-
assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);
747+
assertLog([]);
748748

749749
await act(async () => {
750750
resolvePromise();

packages/react-dom/src/__tests__/ReactDOMForm-test.js

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -699,15 +699,7 @@ describe('ReactDOMForm', () => {
699699
// This should suspend because form actions are implicitly wrapped
700700
// in startTransition.
701701
await submit(formRef.current);
702-
assertLog([
703-
'Pending...',
704-
'Suspend! [Updated]',
705-
'Loading...',
706-
707-
...(gate('enableSiblingPrerendering')
708-
? ['Suspend! [Updated]', 'Loading...']
709-
: []),
710-
]);
702+
assertLog(['Pending...', 'Suspend! [Updated]', 'Loading...']);
711703
expect(container.textContent).toBe('Pending...Initial');
712704

713705
await act(() => resolveText('Updated'));
@@ -744,15 +736,7 @@ describe('ReactDOMForm', () => {
744736

745737
// Update
746738
await submit(formRef.current);
747-
assertLog([
748-
'Pending...',
749-
'Suspend! [Count: 1]',
750-
'Loading...',
751-
752-
...(gate('enableSiblingPrerendering')
753-
? ['Suspend! [Count: 1]', 'Loading...']
754-
: []),
755-
]);
739+
assertLog(['Pending...', 'Suspend! [Count: 1]', 'Loading...']);
756740
expect(container.textContent).toBe('Pending...Count: 0');
757741

758742
await act(() => resolveText('Count: 1'));
@@ -761,15 +745,7 @@ describe('ReactDOMForm', () => {
761745

762746
// Update again
763747
await submit(formRef.current);
764-
assertLog([
765-
'Pending...',
766-
'Suspend! [Count: 2]',
767-
'Loading...',
768-
769-
...(gate('enableSiblingPrerendering')
770-
? ['Suspend! [Count: 2]', 'Loading...']
771-
: []),
772-
]);
748+
assertLog(['Pending...', 'Suspend! [Count: 2]', 'Loading...']);
773749
expect(container.textContent).toBe('Pending...Count: 1');
774750

775751
await act(() => resolveText('Count: 2'));
@@ -813,14 +789,7 @@ describe('ReactDOMForm', () => {
813789
assertLog(['Async action started', 'Pending...']);
814790

815791
await act(() => resolveText('Wait'));
816-
assertLog([
817-
'Suspend! [Updated]',
818-
'Loading...',
819-
820-
...(gate('enableSiblingPrerendering')
821-
? ['Suspend! [Updated]', 'Loading...']
822-
: []),
823-
]);
792+
assertLog(['Suspend! [Updated]', 'Loading...']);
824793
expect(container.textContent).toBe('Pending...Initial');
825794

826795
await act(() => resolveText('Updated'));
@@ -1506,15 +1475,7 @@ describe('ReactDOMForm', () => {
15061475
// Now dispatch inside of a transition. This one does not trigger a
15071476
// loading state.
15081477
await act(() => startTransition(() => dispatch()));
1509-
assertLog([
1510-
'Count: 1',
1511-
'Suspend! [Count: 2]',
1512-
'Loading...',
1513-
1514-
...(gate('enableSiblingPrerendering')
1515-
? ['Suspend! [Count: 2]', 'Loading...']
1516-
: []),
1517-
]);
1478+
assertLog(['Count: 1', 'Suspend! [Count: 2]', 'Loading...']);
15181479
expect(container.textContent).toBe('Count: 1');
15191480

15201481
await act(() => resolveText('Count: 2'));

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,7 +1666,7 @@ function mountSyncExternalStore<T>(
16661666
}
16671667

16681668
const rootRenderLanes = getWorkInProgressRootRenderLanes();
1669-
if (!includesBlockingLane(root, rootRenderLanes)) {
1669+
if (!includesBlockingLane(rootRenderLanes)) {
16701670
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
16711671
}
16721672
}
@@ -1778,7 +1778,7 @@ function updateSyncExternalStore<T>(
17781778
);
17791779
}
17801780

1781-
if (!isHydrating && !includesBlockingLane(root, renderLanes)) {
1781+
if (!isHydrating && !includesBlockingLane(renderLanes)) {
17821782
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
17831783
}
17841784
}

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ export function includesOnlyTransitions(lanes: Lanes): boolean {
579579
return (lanes & TransitionLanes) === lanes;
580580
}
581581

582-
export function includesBlockingLane(root: FiberRoot, lanes: Lanes): boolean {
582+
export function includesBlockingLane(lanes: Lanes): boolean {
583583
const SyncDefaultLanes =
584584
InputContinuousHydrationLane |
585585
InputContinuousLane |

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ export function performConcurrentWorkOnRoot(
907907
// bug we're still investigating. Once the bug in Scheduler is fixed,
908908
// we can remove this, since we track expiration ourselves.
909909
const shouldTimeSlice =
910-
!includesBlockingLane(root, lanes) &&
910+
!includesBlockingLane(lanes) &&
911911
!includesExpiredLane(root, lanes) &&
912912
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
913913
let exitStatus = shouldTimeSlice
@@ -1968,6 +1968,18 @@ export function renderDidSuspend(): void {
19681968
export function renderDidSuspendDelayIfPossible(): void {
19691969
workInProgressRootExitStatus = RootSuspendedWithDelay;
19701970

1971+
if (
1972+
!workInProgressRootDidSkipSuspendedSiblings &&
1973+
!includesBlockingLane(workInProgressRootRenderLanes)
1974+
) {
1975+
// This render may not have originally been scheduled as a prerender, but
1976+
// something suspended inside the visible part of the tree, which means we
1977+
// won't be able to commit a fallback anyway. Let's proceed as if this were
1978+
// a prerender so that we can warm up the siblings without scheduling a
1979+
// separate pass.
1980+
workInProgressRootIsPrerendering = true;
1981+
}
1982+
19711983
// Check if there are updates that we skipped tree that might have unblocked
19721984
// this render.
19731985
if (

packages/react-reconciler/src/__tests__/ActivitySuspense-test.js

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,7 @@ describe('Activity Suspense', () => {
215215
);
216216
});
217217
});
218-
assertLog([
219-
'Open',
220-
'Suspend! [Async]',
221-
'Loading...',
222-
223-
...(gate('enableSiblingPrerendering')
224-
? ['Open', 'Suspend! [Async]', 'Loading...']
225-
: []),
226-
]);
218+
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
227219
// It should suspend with delay to prevent the already-visible Suspense
228220
// boundary from switching to a fallback
229221
expect(root).toMatchRenderedOutput(<span>Closed</span>);
@@ -284,15 +276,7 @@ describe('Activity Suspense', () => {
284276
);
285277
});
286278
});
287-
assertLog([
288-
'Open',
289-
'Suspend! [Async]',
290-
'Loading...',
291-
292-
...(gate('enableSiblingPrerendering')
293-
? ['Open', 'Suspend! [Async]', 'Loading...']
294-
: []),
295-
]);
279+
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
296280
// It should suspend with delay to prevent the already-visible Suspense
297281
// boundary from switching to a fallback
298282
expect(root).toMatchRenderedOutput(

packages/react-reconciler/src/__tests__/ReactActWarnings-test.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,7 @@ describe('act warnings', () => {
349349
root.render(<App showMore={true} />);
350350
});
351351
});
352-
assertLog([
353-
'Suspend! [Async]',
354-
'Loading...',
355-
356-
...(gate('enableSiblingPrerendering')
357-
? ['Suspend! [Async]', 'Loading...']
358-
: []),
359-
]);
352+
assertLog(['Suspend! [Async]', 'Loading...']);
360353
expect(root).toMatchRenderedOutput('(empty)');
361354

362355
// This is a ping, not a retry, because no fallback is showing.

packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ describe('ReactAsyncActions', () => {
303303
'Suspend! [A1]',
304304

305305
...(gate('enableSiblingPrerendering')
306-
? ['Pending: false', 'Suspend! [A1]', 'Suspend! [B1]', 'Suspend! [C1]']
306+
? ['Suspend! [B1]', 'Suspend! [C1]']
307307
: []),
308308
]);
309309
expect(root).toMatchRenderedOutput(
@@ -322,9 +322,7 @@ describe('ReactAsyncActions', () => {
322322
'A1',
323323
'Suspend! [B1]',
324324

325-
...(gate('enableSiblingPrerendering')
326-
? ['Pending: false', 'A1', 'Suspend! [B1]', 'Suspend! [C1]']
327-
: []),
325+
...(gate('enableSiblingPrerendering') ? ['Suspend! [C1]'] : []),
328326
]);
329327
expect(root).toMatchRenderedOutput(
330328
<>
@@ -333,16 +331,7 @@ describe('ReactAsyncActions', () => {
333331
</>,
334332
);
335333
await act(() => resolveText('B1'));
336-
assertLog([
337-
'Pending: false',
338-
'A1',
339-
'B1',
340-
'Suspend! [C1]',
341-
342-
...(gate('enableSiblingPrerendering')
343-
? ['Pending: false', 'A1', 'B1', 'Suspend! [C1]']
344-
: []),
345-
]);
334+
assertLog(['Pending: false', 'A1', 'B1', 'Suspend! [C1]']);
346335
expect(root).toMatchRenderedOutput(
347336
<>
348337
<span>Pending: true</span>
@@ -715,10 +704,6 @@ describe('ReactAsyncActions', () => {
715704
// automatically reverted.
716705
'Pending: false',
717706
'Suspend! [B]',
718-
719-
...(gate('enableSiblingPrerendering')
720-
? ['Pending: false', 'Suspend! [B]']
721-
: []),
722707
]);
723708

724709
// Resolve the transition

packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -209,16 +209,7 @@ describe('ReactConcurrentErrorRecovery', () => {
209209
root.render(<App step={2} />);
210210
});
211211
});
212-
assertLog([
213-
'Suspend! [A2]',
214-
'Loading...',
215-
'Suspend! [B2]',
216-
'Loading...',
217-
218-
...(gate('enableSiblingPrerendering')
219-
? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
220-
: []),
221-
]);
212+
assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
222213
// Because this is a refresh, we don't switch to a fallback
223214
expect(root).toMatchRenderedOutput('A1B1');
224215

@@ -229,16 +220,7 @@ describe('ReactConcurrentErrorRecovery', () => {
229220

230221
// Because we're still suspended on A, we can't show an error boundary. We
231222
// should wait for A to resolve.
232-
assertLog([
233-
'Suspend! [A2]',
234-
'Loading...',
235-
'Error! [B2]',
236-
'Oops!',
237-
238-
...(gate('enableSiblingPrerendering')
239-
? ['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']
240-
: []),
241-
]);
223+
assertLog(['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']);
242224
// Remain on previous screen.
243225
expect(root).toMatchRenderedOutput('A1B1');
244226

@@ -299,16 +281,7 @@ describe('ReactConcurrentErrorRecovery', () => {
299281
root.render(<App step={2} />);
300282
});
301283
});
302-
assertLog([
303-
'Suspend! [A2]',
304-
'Loading...',
305-
'Suspend! [B2]',
306-
'Loading...',
307-
308-
...(gate('enableSiblingPrerendering')
309-
? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
310-
: []),
311-
]);
284+
assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
312285
// Because this is a refresh, we don't switch to a fallback
313286
expect(root).toMatchRenderedOutput('A1B1');
314287

@@ -364,11 +337,7 @@ describe('ReactConcurrentErrorRecovery', () => {
364337
root.render(<AsyncText text="Async" />);
365338
});
366339
});
367-
assertLog([
368-
'Suspend! [Async]',
369-
370-
...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
371-
]);
340+
assertLog(['Suspend! [Async]']);
372341
expect(root).toMatchRenderedOutput(null);
373342

374343
// This also works if the suspended component is wrapped with an error
@@ -384,11 +353,7 @@ describe('ReactConcurrentErrorRecovery', () => {
384353
);
385354
});
386355
});
387-
assertLog([
388-
'Suspend! [Async]',
389-
390-
...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
391-
]);
356+
assertLog(['Suspend! [Async]']);
392357
expect(root).toMatchRenderedOutput(null);
393358

394359
// Continues rendering once data resolves
@@ -445,7 +410,7 @@ describe('ReactConcurrentErrorRecovery', () => {
445410
'Suspend! [Async]',
446411

447412
...(gate('enableSiblingPrerendering')
448-
? ['Suspend! [Async]', 'Caught an error: Oops!']
413+
? ['Caught an error: Oops!']
449414
: []),
450415
]);
451416
// The render suspended without committing the error.
@@ -468,7 +433,7 @@ describe('ReactConcurrentErrorRecovery', () => {
468433
'Suspend! [Async]',
469434

470435
...(gate('enableSiblingPrerendering')
471-
? ['Suspend! [Async]', 'Caught an error: Oops!']
436+
? ['Caught an error: Oops!']
472437
: []),
473438
]);
474439
expect(root).toMatchRenderedOutput(null);

0 commit comments

Comments
 (0)