Skip to content

Commit 7e14f7c

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 7e14f7c

20 files changed

+239
-558
lines changed

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

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -233,22 +233,10 @@ describe('ReactCache', () => {
233233
</Suspense>,
234234
);
235235

236-
await waitForAll([1, 'Suspend! [4]', 'Loading...']);
236+
await waitForAll([1, 'Suspend! [4]', 'Suspend! [5]', 'Loading...']);
237237

238238
await act(() => jest.advanceTimersByTime(100));
239-
assertLog([
240-
'Promise resolved [4]',
241-
1,
242-
4,
243-
'Suspend! [5]',
244-
1,
245-
4,
246-
'Suspend! [5]',
247-
'Promise resolved [5]',
248-
1,
249-
4,
250-
5,
251-
]);
239+
assertLog(['Promise resolved [4]', 'Promise resolved [5]', 1, 4, 5]);
252240

253241
expect(root).toMatchRenderedOutput('145');
254242

@@ -268,23 +256,12 @@ describe('ReactCache', () => {
268256
1,
269257
// 2 and 3 suspend because they were evicted from the cache
270258
'Suspend! [2]',
259+
'Suspend! [3]',
271260
'Loading...',
272261
]);
273262

274263
await act(() => jest.advanceTimersByTime(100));
275-
assertLog([
276-
'Promise resolved [2]',
277-
1,
278-
2,
279-
'Suspend! [3]',
280-
1,
281-
2,
282-
'Suspend! [3]',
283-
'Promise resolved [3]',
284-
1,
285-
2,
286-
3,
287-
]);
264+
assertLog(['Promise resolved [2]', 'Promise resolved [3]', 1, 2, 3]);
288265
expect(root).toMatchRenderedOutput('123');
289266
});
290267

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: 7 additions & 54 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'));
@@ -1534,11 +1495,7 @@ describe('ReactDOMForm', () => {
15341495

15351496
const root = ReactDOMClient.createRoot(container);
15361497
await act(() => root.render(<App />));
1537-
assertLog([
1538-
'Suspend! [Count: 0]',
1539-
1540-
...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 0]'] : []),
1541-
]);
1498+
assertLog(['Suspend! [Count: 0]']);
15421499
await act(() => resolveText('Count: 0'));
15431500
assertLog(['Count: 0']);
15441501

@@ -1551,11 +1508,7 @@ describe('ReactDOMForm', () => {
15511508
{withoutStack: true},
15521509
],
15531510
]);
1554-
assertLog([
1555-
'Suspend! [Count: 1]',
1556-
1557-
...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 1]'] : []),
1558-
]);
1511+
assertLog(['Suspend! [Count: 1]']);
15591512
expect(container.textContent).toBe('Count: 0');
15601513
});
15611514

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)