Skip to content

Commit 50389e1

Browse files
authored
[Fizz] Hoist hoistables to each row and transfer the dependencies to future rows (facebook#33312)
Stacked on facebook#33311. When a row contains Suspense boundaries that themselves depend on CSS, they will not resolve until the CSS has loaded on the client. We need future rows in a list to be blocked until this happens. We could do something in the runtime but a simpler approach is to just add those CSS dependencies to all those boundaries as well. To do this, we first hoist the HoistableState from a completed boundary onto its parent row. Then when the row finishes do we hoist it onto the next row and onto any boundaries within that row.
1 parent 99aa685 commit 50389e1

File tree

2 files changed

+210
-5
lines changed

2 files changed

+210
-5
lines changed

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

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ let ReactDOM;
2121
let ReactDOMClient;
2222
let ReactDOMFizzServer;
2323
let Suspense;
24+
let SuspenseList;
2425
let textCache;
2526
let loadCache;
2627
let writable;
@@ -74,6 +75,7 @@ describe('ReactDOMFloat', () => {
7475
ReactDOMFizzServer = require('react-dom/server');
7576
Stream = require('stream');
7677
Suspense = React.Suspense;
78+
SuspenseList = React.unstable_SuspenseList;
7779
Scheduler = require('scheduler/unstable_mock');
7880

7981
const InternalTestUtils = require('internal-test-utils');
@@ -5746,6 +5748,181 @@ body {
57465748
);
57475749
});
57485750

5751+
// @gate enableSuspenseList
5752+
it('delays "forwards" SuspenseList rows until the css of previous rows have completed', async () => {
5753+
await act(() => {
5754+
renderToPipeableStream(
5755+
<html>
5756+
<body>
5757+
<Suspense fallback="loading...">
5758+
<SuspenseList revealOrder="forwards">
5759+
<Suspense fallback="loading foo...">
5760+
<BlockedOn value="foo">
5761+
<link rel="stylesheet" href="foo" precedence="foo" />
5762+
foo
5763+
</BlockedOn>
5764+
</Suspense>
5765+
<Suspense fallback="loading bar...">bar</Suspense>
5766+
<BlockedOn value="bar">
5767+
<Suspense fallback="loading baz...">
5768+
<BlockedOn value="baz">baz</BlockedOn>
5769+
</Suspense>
5770+
</BlockedOn>
5771+
</SuspenseList>
5772+
</Suspense>
5773+
</body>
5774+
</html>,
5775+
).pipe(writable);
5776+
});
5777+
5778+
expect(getMeaningfulChildren(document)).toEqual(
5779+
<html>
5780+
<head />
5781+
<body>loading...</body>
5782+
</html>,
5783+
);
5784+
5785+
// unblock css loading
5786+
await act(() => {
5787+
resolveText('foo');
5788+
});
5789+
5790+
// bar is still blocking the whole list
5791+
expect(getMeaningfulChildren(document)).toEqual(
5792+
<html>
5793+
<head>
5794+
<link rel="stylesheet" href="foo" data-precedence="foo" />
5795+
</head>
5796+
<body>
5797+
{'loading...'}
5798+
<link as="style" href="foo" rel="preload" />
5799+
</body>
5800+
</html>,
5801+
);
5802+
5803+
// unblock inner loading states
5804+
await act(() => {
5805+
resolveText('bar');
5806+
});
5807+
5808+
expect(getMeaningfulChildren(document)).toEqual(
5809+
<html>
5810+
<head>
5811+
<link rel="stylesheet" href="foo" data-precedence="foo" />
5812+
</head>
5813+
<body>
5814+
{'loading foo...'}
5815+
{'loading bar...'}
5816+
{'loading baz...'}
5817+
<link as="style" href="foo" rel="preload" />
5818+
</body>
5819+
</html>,
5820+
);
5821+
5822+
// resolve the last boundary
5823+
await act(() => {
5824+
resolveText('baz');
5825+
});
5826+
5827+
// still blocked on the css of the first row
5828+
expect(getMeaningfulChildren(document)).toEqual(
5829+
<html>
5830+
<head>
5831+
<link rel="stylesheet" href="foo" data-precedence="foo" />
5832+
</head>
5833+
<body>
5834+
{'loading foo...'}
5835+
{'loading bar...'}
5836+
{'loading baz...'}
5837+
<link as="style" href="foo" rel="preload" />
5838+
</body>
5839+
</html>,
5840+
);
5841+
5842+
await act(() => {
5843+
loadStylesheets();
5844+
});
5845+
await assertLog(['load stylesheet: foo']);
5846+
expect(getMeaningfulChildren(document)).toEqual(
5847+
<html>
5848+
<head>
5849+
<link rel="stylesheet" href="foo" data-precedence="foo" />
5850+
</head>
5851+
<body>
5852+
{'foo'}
5853+
{'bar'}
5854+
{'baz'}
5855+
<link as="style" href="foo" rel="preload" />
5856+
</body>
5857+
</html>,
5858+
);
5859+
});
5860+
5861+
// @gate enableSuspenseList
5862+
it('delays "together" SuspenseList rows until the css of previous rows have completed', async () => {
5863+
await act(() => {
5864+
renderToPipeableStream(
5865+
<html>
5866+
<body>
5867+
<SuspenseList revealOrder="together">
5868+
<Suspense fallback="loading foo...">
5869+
<BlockedOn value="foo">
5870+
<link rel="stylesheet" href="foo" precedence="foo" />
5871+
foo
5872+
</BlockedOn>
5873+
</Suspense>
5874+
<Suspense fallback="loading bar...">bar</Suspense>
5875+
</SuspenseList>
5876+
</body>
5877+
</html>,
5878+
).pipe(writable);
5879+
});
5880+
5881+
expect(getMeaningfulChildren(document)).toEqual(
5882+
<html>
5883+
<head />
5884+
<body>
5885+
{'loading foo...'}
5886+
{'loading bar...'}
5887+
</body>
5888+
</html>,
5889+
);
5890+
5891+
await act(() => {
5892+
resolveText('foo');
5893+
});
5894+
5895+
expect(getMeaningfulChildren(document)).toEqual(
5896+
<html>
5897+
<head>
5898+
<link rel="stylesheet" href="foo" data-precedence="foo" />
5899+
</head>
5900+
<body>
5901+
{'loading foo...'}
5902+
{'loading bar...'}
5903+
<link as="style" href="foo" rel="preload" />
5904+
</body>
5905+
</html>,
5906+
);
5907+
5908+
await act(() => {
5909+
loadStylesheets();
5910+
});
5911+
await assertLog(['load stylesheet: foo']);
5912+
expect(getMeaningfulChildren(document)).toEqual(
5913+
<html>
5914+
<head>
5915+
<link rel="stylesheet" href="foo" data-precedence="foo" />
5916+
</head>
5917+
<body>
5918+
{'foo'}
5919+
{'bar'}
5920+
<link as="style" href="foo" rel="preload" />
5921+
</body>
5922+
</html>,
5923+
);
5924+
});
5925+
57495926
describe('ReactDOM.preconnect(href, { crossOrigin })', () => {
57505927
it('creates a preconnect resource when called', async () => {
57515928
function App({url}) {

packages/react-server/src/ReactFizzServer.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ type LegacyContext = {
236236
type SuspenseListRow = {
237237
pendingTasks: number, // The number of tasks, previous rows and inner suspense boundaries blocking this row.
238238
boundaries: null | Array<SuspenseBoundary>, // The boundaries in this row waiting to be unblocked by the previous row. (null means this row is not blocked)
239+
hoistables: HoistableState, // Any dependencies that this row depends on. Future rows need to also depend on it.
240+
inheritedHoistables: null | HoistableState, // Any dependencies that previous row depend on, that new boundaries of this row needs.
239241
together: boolean, // All the boundaries within this row must be revealed together.
240242
next: null | SuspenseListRow, // The next row blocked by this one.
241243
};
@@ -790,6 +792,10 @@ function createSuspenseBoundary(
790792
boundary.pendingTasks++;
791793
blockedBoundaries.push(boundary);
792794
}
795+
const inheritedHoistables = row.inheritedHoistables;
796+
if (inheritedHoistables !== null) {
797+
hoistHoistables(boundary.contentState, inheritedHoistables);
798+
}
793799
}
794800
return boundary;
795801
}
@@ -1676,22 +1682,36 @@ function replaySuspenseBoundary(
16761682

16771683
function finishSuspenseListRow(request: Request, row: SuspenseListRow): void {
16781684
// This row finished. Now we have to unblock all the next rows that were blocked on this.
1679-
unblockSuspenseListRow(request, row.next);
1685+
unblockSuspenseListRow(request, row.next, row.hoistables);
16801686
}
16811687

16821688
function unblockSuspenseListRow(
16831689
request: Request,
16841690
unblockedRow: null | SuspenseListRow,
1691+
inheritedHoistables: null | HoistableState,
16851692
): void {
16861693
// We do this in a loop to avoid stack overflow for very long lists that get unblocked.
16871694
while (unblockedRow !== null) {
1695+
if (inheritedHoistables !== null) {
1696+
// Hoist any hoistables from the previous row into the next row so that it can be
1697+
// later transferred to all the rows.
1698+
hoistHoistables(unblockedRow.hoistables, inheritedHoistables);
1699+
// Mark the row itself for any newly discovered Suspense boundaries to inherit.
1700+
// This is different from hoistables because that also includes hoistables from
1701+
// all the boundaries below this row and not just previous rows.
1702+
unblockedRow.inheritedHoistables = inheritedHoistables;
1703+
}
16881704
// Unblocking the boundaries will decrement the count of this row but we keep it above
16891705
// zero so they never finish this row recursively.
16901706
const unblockedBoundaries = unblockedRow.boundaries;
16911707
if (unblockedBoundaries !== null) {
16921708
unblockedRow.boundaries = null;
16931709
for (let i = 0; i < unblockedBoundaries.length; i++) {
1694-
finishedTask(request, unblockedBoundaries[i], null, null);
1710+
const unblockedBoundary = unblockedBoundaries[i];
1711+
if (inheritedHoistables !== null) {
1712+
hoistHoistables(unblockedBoundary.contentState, inheritedHoistables);
1713+
}
1714+
finishedTask(request, unblockedBoundary, null, null);
16951715
}
16961716
}
16971717
// Instead we decrement at the end to keep it all in this loop.
@@ -1700,6 +1720,7 @@ function unblockSuspenseListRow(
17001720
// Still blocked.
17011721
break;
17021722
}
1723+
inheritedHoistables = unblockedRow.hoistables;
17031724
unblockedRow = unblockedRow.next;
17041725
}
17051726
}
@@ -1728,7 +1749,7 @@ function tryToResolveTogetherRow(
17281749
}
17291750
}
17301751
if (allCompleteAndInlinable) {
1731-
unblockSuspenseListRow(request, togetherRow);
1752+
unblockSuspenseListRow(request, togetherRow, togetherRow.hoistables);
17321753
}
17331754
}
17341755

@@ -1738,6 +1759,8 @@ function createSuspenseListRow(
17381759
const newRow: SuspenseListRow = {
17391760
pendingTasks: 1, // At first the row is blocked on attempting rendering itself.
17401761
boundaries: null,
1762+
hoistables: createHoistableState(),
1763+
inheritedHoistables: null,
17411764
together: false,
17421765
next: null,
17431766
};
@@ -4869,10 +4892,15 @@ function finishedTask(
48694892
// If the boundary is eligible to be outlined during flushing we can't cancel the fallback
48704893
// since we might need it when it's being outlined.
48714894
if (boundary.status === COMPLETED) {
4895+
const boundaryRow = boundary.row;
4896+
if (boundaryRow !== null) {
4897+
// Hoist the HoistableState from the boundary to the row so that the next rows
4898+
// can depend on the same dependencies.
4899+
hoistHoistables(boundaryRow.hoistables, boundary.contentState);
4900+
}
48724901
if (!isEligibleForOutlining(request, boundary)) {
48734902
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
48744903
boundary.fallbackAbortableTasks.clear();
4875-
const boundaryRow = boundary.row;
48764904
if (boundaryRow !== null) {
48774905
// If we aren't eligible for outlining, we don't have to wait until we flush it.
48784906
if (--boundaryRow.pendingTasks === 0) {
@@ -5679,7 +5707,7 @@ function flushPartialBoundary(
56795707
// unblock the boundary itself which can issue its complete instruction.
56805708
// TODO: Ideally the complete instruction would be in a single <script> tag.
56815709
if (row.pendingTasks === 1) {
5682-
unblockSuspenseListRow(request, row);
5710+
unblockSuspenseListRow(request, row, row.hoistables);
56835711
} else {
56845712
row.pendingTasks--;
56855713
}

0 commit comments

Comments
 (0)