Files
react/packages/react-markup/src/ReactFizzConfigMarkup.js
Sebastian Markbåge 6eb5d67e9c [Fizz] Outline a Suspense Boundary if it has Suspensey CSS or Images (#34552)
We should favor outlining a boundary if it contains Suspensey CSS or
Suspensey Images since then we can load that content separately and not
block the main content. This also allows us to animate the reveal.

For example this should be able to animate the reveal even though the
actual HTML content isn't large in this case it's worth outlining so
that the JS runtime can choose to animate this reveal.

```js
<ViewTransition>
  <Suspense>
    <img src="..." />
  </Suspense>
</ViewTransition>
```

For Suspensey Images, in Fizz, we currently only implement the suspensey
semantics when a View Transition is running. Therefore the outlining
only applies if it appears inside a Suspense boundary which might
animate. Otherwise there's no point in outlining. It is also only if the
Suspense boundary itself might animate its appear and not just any
ViewTransition. So the effect is very conservative.

For CSS it applies even without ViewTransition though, since it can help
unblock the main content faster.
2025-09-25 09:38:41 -04:00

252 lines
6.3 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactNodeList} from 'shared/ReactTypes';
import type {
RenderState,
ResumableState,
PreambleState,
HoistableState,
FormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
import {
pushStartInstance as pushStartInstanceImpl,
writePreambleStart as writePreambleStartImpl,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
import type {
Destination,
Chunk,
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
import hasOwnProperty from 'shared/hasOwnProperty';
// Allow embedding inside another Fizz render.
export const isPrimaryRenderer = false;
// Disable Client Hooks
export const supportsClientAPIs = false;
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
export type {
RenderState,
ResumableState,
HoistableState,
PreambleState,
FormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
export {
getChildFormatContext,
getSuspenseFallbackFormatContext,
getSuspenseContentFormatContext,
makeId,
pushEndInstance,
pushFormStateMarkerIsMatching,
pushFormStateMarkerIsNotMatching,
writeStartSegment,
writeEndSegment,
writeCompletedSegmentInstruction,
writeCompletedBoundaryInstruction,
writeClientRenderBoundaryInstruction,
writeStartPendingSuspenseBoundary,
writeEndPendingSuspenseBoundary,
writeHoistablesForBoundary,
writePlaceholder,
createRootFormatContext,
createRenderState,
createResumableState,
createPreambleState,
createHoistableState,
writePreambleEnd,
writeHoistables,
writePostamble,
hoistHoistables,
resetResumableState,
completeResumableState,
emitEarlyPreloads,
doctypeChunk,
canHavePreamble,
hoistPreambleState,
isPreambleReady,
isPreambleContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
import escapeTextForBrowser from 'react-dom-bindings/src/server/escapeTextForBrowser';
export function getViewTransitionFormatContext(
resumableState: ResumableState,
parentContext: FormatContext,
update: void | null | 'none' | 'auto' | string,
enter: void | null | 'none' | 'auto' | string,
exit: void | null | 'none' | 'auto' | string,
share: void | null | 'none' | 'auto' | string,
name: void | null | 'auto' | string,
autoName: string, // name or an autogenerated unique name
): FormatContext {
// ViewTransition reveals are not supported in markup renders.
return parentContext;
}
export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
preambleState: null | PreambleState,
hoistableState: null | HoistableState,
formatContext: FormatContext,
textEmbedded: boolean,
): ReactNodeList {
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propKey === 'ref' && propValue != null) {
throw new Error(
'Cannot pass ref in renderToHTML because they will never be hydrated.',
);
}
if (typeof propValue === 'function') {
throw new Error(
'Cannot pass event handlers (' +
propKey +
') in renderToHTML because ' +
'the HTML will never be hydrated so they can never get called.',
);
}
}
}
return pushStartInstanceImpl(
target,
type,
props,
resumableState,
renderState,
preambleState,
hoistableState,
formatContext,
textEmbedded,
);
}
export function pushTextInstance(
target: Array<Chunk | PrecomputedChunk>,
text: string,
renderState: RenderState,
textEmbedded: boolean,
): boolean {
// Markup doesn't need any termination.
target.push(stringToChunk(escapeTextForBrowser(text)));
return false;
}
export function pushSegmentFinale(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
lastPushedText: boolean,
textEmbedded: boolean,
): void {
// Markup doesn't need any termination.
return;
}
export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
// Markup doesn't have any instructions.
return;
}
export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
// Markup doesn't have any instructions.
return;
}
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
// Markup doesn't have any instructions.
return true;
}
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
// flushing these error arguments are not currently supported in this legacy streaming format.
errorDigest: ?string,
errorMessage: ?string,
errorStack: ?string,
errorComponentStack: ?string,
): boolean {
// Markup doesn't have any instructions.
return true;
}
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
// Markup doesn't have any instructions.
return true;
}
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
// Markup doesn't have any instructions.
return true;
}
export function writePreambleStart(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
skipBlockingShell: boolean,
): void {
return writePreambleStartImpl(
destination,
resumableState,
renderState,
true, // skipBlockingShell
);
}
export function writeCompletedRoot(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
isComplete: boolean,
): boolean {
// Markup doesn't have any bootstrap scripts nor shell completions.
return true;
}
export function hasSuspenseyContent(hoistableState: HoistableState): boolean {
// Never outline.
return false;
}
export type TransitionStatus = FormStatus;
export const NotPendingTransition: TransitionStatus = NotPending;