Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/compiler_bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ body:
options:
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
- type: input
attributes:
Expand Down
30 changes: 30 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9433,4 +9433,34 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue
</html>,
);
});

// @gate enableCPUSuspense
it('outlines deferred Suspense boundaries', async () => {
function Log({text}) {
Scheduler.log(text);
return text;
}

await act(async () => {
renderToPipeableStream(
<div>
<Suspense defer={true} fallback={<Log text="Waiting" />}>
<span>{<Log text="hello" />}</span>
</Suspense>
</div>,
).pipe(writable);
await jest.runAllTimers();
const temp = document.createElement('body');
temp.innerHTML = buffer;
expect(getVisibleChildren(temp)).toEqual(<div>Waiting</div>);
});

assertLog(['Waiting', 'hello']);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>hello</span>
</div>,
);
});
});
5 changes: 1 addition & 4 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -2456,10 +2456,7 @@ function updateSuspenseComponent(
}

return bailoutOffscreenComponent(null, primaryChildFragment);
} else if (
enableCPUSuspense &&
typeof nextProps.unstable_expectedLoadTime === 'number'
) {
} else if (enableCPUSuspense && nextProps.defer === true) {
// This is a CPU-bound tree. Skip this tree and show a placeholder to
// unblock the surrounding content. Then immediately retry after the
// initial commit.
Expand Down
22 changes: 7 additions & 15 deletions packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable react/jsx-boolean-value */

let React;
let ReactNoop;
let Scheduler;
Expand Down Expand Up @@ -122,9 +124,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
<>
<Text text="Outer" />
<div>
<Suspense
unstable_expectedLoadTime={2000}
fallback={<Text text="Loading..." />}>
<Suspense defer fallback={<Text text="Loading..." />}>
<Text text="Inner" />
</Suspense>
</div>
Expand Down Expand Up @@ -164,9 +164,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
<>
<Text text="Outer" />
<div>
<Suspense
unstable_expectedLoadTime={2000}
fallback={<Text text="Loading..." />}>
<Suspense defer fallback={<Text text="Loading..." />}>
<Text text={`Inner [${count}]`} />
</Suspense>
</div>
Expand Down Expand Up @@ -209,9 +207,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
<>
<Text text="Outer" />
<div>
<Suspense
unstable_expectedLoadTime={2000}
fallback={<Text text="Loading..." />}>
<Suspense defer fallback={<Text text="Loading..." />}>
<AsyncText text="Inner" />
</Suspense>
</div>
Expand Down Expand Up @@ -263,14 +259,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
<>
<Text text="A" />
<div>
<Suspense
unstable_expectedLoadTime={2000}
fallback={<Text text="Loading B..." />}>
<Suspense defer fallback={<Text text="Loading B..." />}>
<Text text="B" />
<div>
<Suspense
unstable_expectedLoadTime={2000}
fallback={<Text text="Loading C..." />}>
<Suspense defer fallback={<Text text="Loading C..." />}>
<Text text="C" />
</Suspense>
</div>
Expand Down
51 changes: 34 additions & 17 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ import {
enableViewTransition,
enableFizzBlockingRender,
enableAsyncDebugInfo,
enableCPUSuspense,
} from 'shared/ReactFeatureFlags';

import assign from 'shared/assign';
Expand Down Expand Up @@ -250,6 +251,7 @@ type SuspenseBoundary = {
row: null | SuspenseListRow, // the row that this boundary blocks from completing.
completedSegments: Array<Segment>, // completed but not yet flushed segments.
byteSize: number, // used to determine whether to inline children boundaries.
defer: boolean, // never inline deferred boundaries
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
contentState: HoistableState,
fallbackState: HoistableState,
Expand Down Expand Up @@ -456,7 +458,9 @@ function isEligibleForOutlining(
// The larger this limit is, the more we can save on preparing fallbacks in case we end up
// outlining.
return (
(boundary.byteSize > 500 || hasSuspenseyContent(boundary.contentState)) &&
(boundary.byteSize > 500 ||
hasSuspenseyContent(boundary.contentState) ||
boundary.defer) &&
// For boundaries that can possibly contribute to the preamble we don't want to outline
// them regardless of their size since the fallbacks should only be emitted if we've
// errored the boundary.
Expand Down Expand Up @@ -782,6 +786,7 @@ function createSuspenseBoundary(
fallbackAbortableTasks: Set<Task>,
contentPreamble: null | Preamble,
fallbackPreamble: null | Preamble,
defer: boolean,
): SuspenseBoundary {
const boundary: SuspenseBoundary = {
status: PENDING,
Expand All @@ -791,6 +796,7 @@ function createSuspenseBoundary(
row: row,
completedSegments: [],
byteSize: 0,
defer: defer,
fallbackAbortableTasks,
errorDigest: null,
contentState: createHoistableState(),
Expand Down Expand Up @@ -1274,6 +1280,7 @@ function renderSuspenseBoundary(
// in case it ends up generating a large subtree of content.
const fallback: ReactNodeList = props.fallback;
const content: ReactNodeList = props.children;
const defer: boolean = enableCPUSuspense && props.defer === true;

const fallbackAbortSet: Set<Task> = new Set();
let newBoundary: SuspenseBoundary;
Expand All @@ -1284,6 +1291,7 @@ function renderSuspenseBoundary(
fallbackAbortSet,
createPreambleState(),
createPreambleState(),
defer,
);
} else {
newBoundary = createSuspenseBoundary(
Expand All @@ -1292,6 +1300,7 @@ function renderSuspenseBoundary(
fallbackAbortSet,
null,
null,
defer,
);
}
if (request.trackedPostpones !== null) {
Expand Down Expand Up @@ -1327,29 +1336,32 @@ function renderSuspenseBoundary(
// no parent segment so there's nothing to wait on.
contentRootSegment.parentFlushed = true;

if (request.trackedPostpones !== null) {
const trackedPostpones = request.trackedPostpones;
if (trackedPostpones !== null || defer) {
// This is a prerender or deferred boundary. In this mode we want to render the fallback synchronously
// and schedule the content to render later. This is the opposite of what we do during a normal render
// where we try to skip rendering the fallback if the content itself can render synchronously

// Stash the original stack frame.
const suspenseComponentStack = task.componentStack;
// This is a prerender. In this mode we want to render the fallback synchronously and schedule
// the content to render later. This is the opposite of what we do during a normal render
// where we try to skip rendering the fallback if the content itself can render synchronously
const trackedPostpones = request.trackedPostpones;

const fallbackKeyPath: KeyNode = [
keyPath[0],
'Suspense Fallback',
keyPath[2],
];
const fallbackReplayNode: ReplayNode = [
fallbackKeyPath[1],
fallbackKeyPath[2],
([]: Array<ReplayNode>),
null,
];
trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);
// We are rendering the fallback before the boundary content so we keep track of
// the fallback replay node until we determine if the primary content suspends
newBoundary.trackedFallbackNode = fallbackReplayNode;
if (trackedPostpones !== null) {
const fallbackReplayNode: ReplayNode = [
fallbackKeyPath[1],
fallbackKeyPath[2],
([]: Array<ReplayNode>),
null,
];
trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);
// We are rendering the fallback before the boundary content so we keep track of
// the fallback replay node until we determine if the primary content suspends
newBoundary.trackedFallbackNode = fallbackReplayNode;
}

task.blockedSegment = boundarySegment;
task.blockedPreamble = newBoundary.fallbackPreamble;
Expand Down Expand Up @@ -1580,6 +1592,7 @@ function replaySuspenseBoundary(

const content: ReactNodeList = props.children;
const fallback: ReactNodeList = props.fallback;
const defer: boolean = enableCPUSuspense && props.defer === true;

const fallbackAbortSet: Set<Task> = new Set();
let resumedBoundary: SuspenseBoundary;
Expand All @@ -1590,6 +1603,7 @@ function replaySuspenseBoundary(
fallbackAbortSet,
createPreambleState(),
createPreambleState(),
defer,
);
} else {
resumedBoundary = createSuspenseBoundary(
Expand All @@ -1598,6 +1612,7 @@ function replaySuspenseBoundary(
fallbackAbortSet,
null,
null,
defer,
);
}
resumedBoundary.parentFlushed = true;
Expand Down Expand Up @@ -4384,6 +4399,7 @@ function abortRemainingSuspenseBoundary(
new Set(),
null,
null,
false,
);
resumedBoundary.parentFlushed = true;
// We restore the same id of this boundary as was used during prerender.
Expand Down Expand Up @@ -5493,7 +5509,8 @@ function flushSegment(
!flushingPartialBoundaries &&
isEligibleForOutlining(request, boundary) &&
(flushedByteSize + boundary.byteSize > request.progressiveChunkSize ||
hasSuspenseyContent(boundary.contentState))
hasSuspenseyContent(boundary.contentState) ||
boundary.defer)
) {
// Inlining this boundary would make the current sequence being written too large
// and block the parent for too long. Instead, it will be emitted separately so that we
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export type SuspenseProps = {
suspenseCallback?: (Set<Wakeable> | null) => mixed,

unstable_avoidThisFallback?: boolean,
unstable_expectedLoadTime?: number,
defer?: boolean,
name?: string,
};

Expand Down
Loading