mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[Fizz] Hoist hoistables to each row and transfer the dependencies to future rows (#33312)
Stacked on #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.
This commit is contained in:
committed by
GitHub
parent
50587474e7
commit
b88fa61d2d
@@ -21,6 +21,7 @@ let ReactDOM;
|
||||
let ReactDOMClient;
|
||||
let ReactDOMFizzServer;
|
||||
let Suspense;
|
||||
let SuspenseList;
|
||||
let textCache;
|
||||
let loadCache;
|
||||
let writable;
|
||||
@@ -74,6 +75,7 @@ describe('ReactDOMFloat', () => {
|
||||
ReactDOMFizzServer = require('react-dom/server');
|
||||
Stream = require('stream');
|
||||
Suspense = React.Suspense;
|
||||
SuspenseList = React.unstable_SuspenseList;
|
||||
Scheduler = require('scheduler/unstable_mock');
|
||||
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
@@ -5746,6 +5748,181 @@ body {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('delays "forwards" SuspenseList rows until the css of previous rows have completed', async () => {
|
||||
await act(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<Suspense fallback="loading...">
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<Suspense fallback="loading foo...">
|
||||
<BlockedOn value="foo">
|
||||
<link rel="stylesheet" href="foo" precedence="foo" />
|
||||
foo
|
||||
</BlockedOn>
|
||||
</Suspense>
|
||||
<Suspense fallback="loading bar...">bar</Suspense>
|
||||
<BlockedOn value="bar">
|
||||
<Suspense fallback="loading baz...">
|
||||
<BlockedOn value="baz">baz</BlockedOn>
|
||||
</Suspense>
|
||||
</BlockedOn>
|
||||
</SuspenseList>
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>loading...</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// unblock css loading
|
||||
await act(() => {
|
||||
resolveText('foo');
|
||||
});
|
||||
|
||||
// bar is still blocking the whole list
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
</head>
|
||||
<body>
|
||||
{'loading...'}
|
||||
<link as="style" href="foo" rel="preload" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// unblock inner loading states
|
||||
await act(() => {
|
||||
resolveText('bar');
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
</head>
|
||||
<body>
|
||||
{'loading foo...'}
|
||||
{'loading bar...'}
|
||||
{'loading baz...'}
|
||||
<link as="style" href="foo" rel="preload" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// resolve the last boundary
|
||||
await act(() => {
|
||||
resolveText('baz');
|
||||
});
|
||||
|
||||
// still blocked on the css of the first row
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
</head>
|
||||
<body>
|
||||
{'loading foo...'}
|
||||
{'loading bar...'}
|
||||
{'loading baz...'}
|
||||
<link as="style" href="foo" rel="preload" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
loadStylesheets();
|
||||
});
|
||||
await assertLog(['load stylesheet: foo']);
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
</head>
|
||||
<body>
|
||||
{'foo'}
|
||||
{'bar'}
|
||||
{'baz'}
|
||||
<link as="style" href="foo" rel="preload" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableSuspenseList
|
||||
it('delays "together" SuspenseList rows until the css of previous rows have completed', async () => {
|
||||
await act(() => {
|
||||
renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<SuspenseList revealOrder="together">
|
||||
<Suspense fallback="loading foo...">
|
||||
<BlockedOn value="foo">
|
||||
<link rel="stylesheet" href="foo" precedence="foo" />
|
||||
foo
|
||||
</BlockedOn>
|
||||
</Suspense>
|
||||
<Suspense fallback="loading bar...">bar</Suspense>
|
||||
</SuspenseList>
|
||||
</body>
|
||||
</html>,
|
||||
).pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
{'loading foo...'}
|
||||
{'loading bar...'}
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
resolveText('foo');
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
</head>
|
||||
<body>
|
||||
{'loading foo...'}
|
||||
{'loading bar...'}
|
||||
<link as="style" href="foo" rel="preload" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
loadStylesheets();
|
||||
});
|
||||
await assertLog(['load stylesheet: foo']);
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
</head>
|
||||
<body>
|
||||
{'foo'}
|
||||
{'bar'}
|
||||
<link as="style" href="foo" rel="preload" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
describe('ReactDOM.preconnect(href, { crossOrigin })', () => {
|
||||
it('creates a preconnect resource when called', async () => {
|
||||
function App({url}) {
|
||||
|
||||
+33
-5
@@ -236,6 +236,8 @@ type LegacyContext = {
|
||||
type SuspenseListRow = {
|
||||
pendingTasks: number, // The number of tasks, previous rows and inner suspense boundaries blocking this row.
|
||||
boundaries: null | Array<SuspenseBoundary>, // The boundaries in this row waiting to be unblocked by the previous row. (null means this row is not blocked)
|
||||
hoistables: HoistableState, // Any dependencies that this row depends on. Future rows need to also depend on it.
|
||||
inheritedHoistables: null | HoistableState, // Any dependencies that previous row depend on, that new boundaries of this row needs.
|
||||
together: boolean, // All the boundaries within this row must be revealed together.
|
||||
next: null | SuspenseListRow, // The next row blocked by this one.
|
||||
};
|
||||
@@ -790,6 +792,10 @@ function createSuspenseBoundary(
|
||||
boundary.pendingTasks++;
|
||||
blockedBoundaries.push(boundary);
|
||||
}
|
||||
const inheritedHoistables = row.inheritedHoistables;
|
||||
if (inheritedHoistables !== null) {
|
||||
hoistHoistables(boundary.contentState, inheritedHoistables);
|
||||
}
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
@@ -1676,22 +1682,36 @@ function replaySuspenseBoundary(
|
||||
|
||||
function finishSuspenseListRow(request: Request, row: SuspenseListRow): void {
|
||||
// This row finished. Now we have to unblock all the next rows that were blocked on this.
|
||||
unblockSuspenseListRow(request, row.next);
|
||||
unblockSuspenseListRow(request, row.next, row.hoistables);
|
||||
}
|
||||
|
||||
function unblockSuspenseListRow(
|
||||
request: Request,
|
||||
unblockedRow: null | SuspenseListRow,
|
||||
inheritedHoistables: null | HoistableState,
|
||||
): void {
|
||||
// We do this in a loop to avoid stack overflow for very long lists that get unblocked.
|
||||
while (unblockedRow !== null) {
|
||||
if (inheritedHoistables !== null) {
|
||||
// Hoist any hoistables from the previous row into the next row so that it can be
|
||||
// later transferred to all the rows.
|
||||
hoistHoistables(unblockedRow.hoistables, inheritedHoistables);
|
||||
// Mark the row itself for any newly discovered Suspense boundaries to inherit.
|
||||
// This is different from hoistables because that also includes hoistables from
|
||||
// all the boundaries below this row and not just previous rows.
|
||||
unblockedRow.inheritedHoistables = inheritedHoistables;
|
||||
}
|
||||
// Unblocking the boundaries will decrement the count of this row but we keep it above
|
||||
// zero so they never finish this row recursively.
|
||||
const unblockedBoundaries = unblockedRow.boundaries;
|
||||
if (unblockedBoundaries !== null) {
|
||||
unblockedRow.boundaries = null;
|
||||
for (let i = 0; i < unblockedBoundaries.length; i++) {
|
||||
finishedTask(request, unblockedBoundaries[i], null, null);
|
||||
const unblockedBoundary = unblockedBoundaries[i];
|
||||
if (inheritedHoistables !== null) {
|
||||
hoistHoistables(unblockedBoundary.contentState, inheritedHoistables);
|
||||
}
|
||||
finishedTask(request, unblockedBoundary, null, null);
|
||||
}
|
||||
}
|
||||
// Instead we decrement at the end to keep it all in this loop.
|
||||
@@ -1700,6 +1720,7 @@ function unblockSuspenseListRow(
|
||||
// Still blocked.
|
||||
break;
|
||||
}
|
||||
inheritedHoistables = unblockedRow.hoistables;
|
||||
unblockedRow = unblockedRow.next;
|
||||
}
|
||||
}
|
||||
@@ -1728,7 +1749,7 @@ function tryToResolveTogetherRow(
|
||||
}
|
||||
}
|
||||
if (allCompleteAndInlinable) {
|
||||
unblockSuspenseListRow(request, togetherRow);
|
||||
unblockSuspenseListRow(request, togetherRow, togetherRow.hoistables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1738,6 +1759,8 @@ function createSuspenseListRow(
|
||||
const newRow: SuspenseListRow = {
|
||||
pendingTasks: 1, // At first the row is blocked on attempting rendering itself.
|
||||
boundaries: null,
|
||||
hoistables: createHoistableState(),
|
||||
inheritedHoistables: null,
|
||||
together: false,
|
||||
next: null,
|
||||
};
|
||||
@@ -4869,10 +4892,15 @@ function finishedTask(
|
||||
// If the boundary is eligible to be outlined during flushing we can't cancel the fallback
|
||||
// since we might need it when it's being outlined.
|
||||
if (boundary.status === COMPLETED) {
|
||||
const boundaryRow = boundary.row;
|
||||
if (boundaryRow !== null) {
|
||||
// Hoist the HoistableState from the boundary to the row so that the next rows
|
||||
// can depend on the same dependencies.
|
||||
hoistHoistables(boundaryRow.hoistables, boundary.contentState);
|
||||
}
|
||||
if (!isEligibleForOutlining(request, boundary)) {
|
||||
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
|
||||
boundary.fallbackAbortableTasks.clear();
|
||||
const boundaryRow = boundary.row;
|
||||
if (boundaryRow !== null) {
|
||||
// If we aren't eligible for outlining, we don't have to wait until we flush it.
|
||||
if (--boundaryRow.pendingTasks === 0) {
|
||||
@@ -5679,7 +5707,7 @@ function flushPartialBoundary(
|
||||
// unblock the boundary itself which can issue its complete instruction.
|
||||
// TODO: Ideally the complete instruction would be in a single <script> tag.
|
||||
if (row.pendingTasks === 1) {
|
||||
unblockSuspenseListRow(request, row);
|
||||
unblockSuspenseListRow(request, row, row.hoistables);
|
||||
} else {
|
||||
row.pendingTasks--;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user