Files
react/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
T
Sebastian Markbåge d177272802 [Fizz] Error and deopt from rel=expect for large documents without boundaries (#33454)
We want to make sure that we can block the reveal of a well designed
complete shell reliably. In the Suspense model, client transitions don't
have any way to implicitly resolve. This means you need to use Suspense
or SuspenseList to explicitly split the document. Relying on implicit
would mean you can't add a Suspense boundary later where needed. So we
highly encourage the use of them around large content.

However, if you have constructed a too large shell (e.g. by not adding
any Suspense boundaries at all) then that might take too long to render
on the client. We shouldn't punish users (or overzealous metrics
tracking tools like search engines) in that scenario.

This opts out of render blocking if the shell ends up too large to be
intentional and too slow to load. Instead it deopts to showing the
content split up in arbitrary ways (browser default). It only does this
for SSR, and not client navs so it's not reliable.

In fact, we issue an error to `onError`. This error is recoverable in
that the document is still produced. It's up to your framework to decide
if this errors the build or just surface it for action later.

What should be the limit though? There's a trade off here. If this limit
is too low then you can't fit a reasonably well built UI within it
without getting errors. If it's too high then things that accidentally
fall below it might take too long to load.

I came up with 512kB of uncompressed shell HTML. See the comment in code
for the rationale for this number. TL;DR: Data and theory indicates that
having this much content inside `rel="expect"` doesn't meaningfully
change metrics. Research of above-the-fold content on various websites
indicate that this can comfortable fit all of them which should be
enough for any intentional initial paint.
2025-06-06 10:29:48 -04:00

330 lines
9.5 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 {
RenderState as BaseRenderState,
ResumableState,
StyleQueue,
Resource,
HeadersDescriptor,
PreambleState,
FormatContext,
} from './ReactFizzConfigDOM';
import {
createRenderState as createRenderStateImpl,
pushTextInstance as pushTextInstanceImpl,
pushSegmentFinale as pushSegmentFinaleImpl,
pushStartActivityBoundary as pushStartActivityBoundaryImpl,
pushEndActivityBoundary as pushEndActivityBoundaryImpl,
writeStartCompletedSuspenseBoundary as writeStartCompletedSuspenseBoundaryImpl,
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
writeEndClientRenderedSuspenseBoundary as writeEndClientRenderedSuspenseBoundaryImpl,
writePreambleStart as writePreambleStartImpl,
} from './ReactFizzConfigDOM';
import type {
Destination,
Chunk,
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
import type {FormStatus} from '../shared/ReactDOMFormActions';
import {NotPending} from '../shared/ReactDOMFormActions';
export const isPrimaryRenderer = false;
export type RenderState = {
// Keep this in sync with ReactFizzConfigDOM
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: PrecomputedChunk,
startInlineScript: PrecomputedChunk,
startInlineStyle: PrecomputedChunk,
preamble: PreambleState,
externalRuntimeScript: null | any,
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
importMapChunks: Array<Chunk | PrecomputedChunk>,
onHeaders: void | ((headers: HeadersDescriptor) => void),
headers: null | {
preconnects: string,
fontPreloads: string,
highImagePreloads: string,
remainingCapacity: number,
},
resets: BaseRenderState['resets'],
charsetChunks: Array<Chunk | PrecomputedChunk>,
viewportChunks: Array<Chunk | PrecomputedChunk>,
hoistableChunks: Array<Chunk | PrecomputedChunk>,
preconnects: Set<Resource>,
fontPreloads: Set<Resource>,
highImagePreloads: Set<Resource>,
// usedImagePreloads: Set<Resource>,
styles: Map<string, StyleQueue>,
bootstrapScripts: Set<Resource>,
scripts: Set<Resource>,
bulkPreloads: Set<Resource>,
preloads: {
images: Map<string, Resource>,
stylesheets: Map<string, Resource>,
scripts: Map<string, Resource>,
moduleScripts: Map<string, Resource>,
},
nonce: {
script: string | void,
style: string | void,
},
stylesToHoist: boolean,
// This is an extra field for the legacy renderer
generateStaticMarkup: boolean,
};
export function createRenderState(
resumableState: ResumableState,
generateStaticMarkup: boolean,
): RenderState {
const renderState = createRenderStateImpl(
resumableState,
undefined,
undefined,
undefined,
undefined,
undefined,
);
return {
// Keep this in sync with ReactFizzConfigDOM
placeholderPrefix: renderState.placeholderPrefix,
segmentPrefix: renderState.segmentPrefix,
boundaryPrefix: renderState.boundaryPrefix,
startInlineScript: renderState.startInlineScript,
startInlineStyle: renderState.startInlineStyle,
preamble: renderState.preamble,
externalRuntimeScript: renderState.externalRuntimeScript,
bootstrapChunks: renderState.bootstrapChunks,
importMapChunks: renderState.importMapChunks,
onHeaders: renderState.onHeaders,
headers: renderState.headers,
resets: renderState.resets,
charsetChunks: renderState.charsetChunks,
viewportChunks: renderState.viewportChunks,
hoistableChunks: renderState.hoistableChunks,
preconnects: renderState.preconnects,
fontPreloads: renderState.fontPreloads,
highImagePreloads: renderState.highImagePreloads,
// usedImagePreloads: renderState.usedImagePreloads,
styles: renderState.styles,
bootstrapScripts: renderState.bootstrapScripts,
scripts: renderState.scripts,
bulkPreloads: renderState.bulkPreloads,
preloads: renderState.preloads,
nonce: renderState.nonce,
stylesToHoist: renderState.stylesToHoist,
// This is an extra field for the legacy renderer
generateStaticMarkup,
};
}
import {
stringToChunk,
stringToPrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
// this chunk is empty on purpose because we do not want to emit the DOCTYPE in legacy mode
export const doctypeChunk: PrecomputedChunk = stringToPrecomputedChunk('');
export type {
ResumableState,
HoistableState,
PreambleState,
FormatContext,
} from './ReactFizzConfigDOM';
export {
getChildFormatContext,
getSuspenseFallbackFormatContext,
getSuspenseContentFormatContext,
makeId,
pushStartInstance,
pushEndInstance,
pushFormStateMarkerIsMatching,
pushFormStateMarkerIsNotMatching,
writeStartSegment,
writeEndSegment,
writeCompletedSegmentInstruction,
writeCompletedBoundaryInstruction,
writeClientRenderBoundaryInstruction,
writeStartPendingSuspenseBoundary,
writeEndPendingSuspenseBoundary,
writeHoistablesForBoundary,
writePlaceholder,
writeCompletedRoot,
createRootFormatContext,
createResumableState,
createPreambleState,
createHoistableState,
writePreambleEnd,
writeHoistables,
writePostamble,
hoistHoistables,
resetResumableState,
completeResumableState,
emitEarlyPreloads,
supportsClientAPIs,
hoistPreambleState,
isPreambleReady,
isPreambleContext,
} from './ReactFizzConfigDOM';
import escapeTextForBrowser from './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 legacy renders.
return parentContext;
}
export function canHavePreamble(formatContext: FormatContext): boolean {
return false;
}
export function pushTextInstance(
target: Array<Chunk | PrecomputedChunk>,
text: string,
renderState: RenderState,
textEmbedded: boolean,
): boolean {
if (renderState.generateStaticMarkup) {
target.push(stringToChunk(escapeTextForBrowser(text)));
return false;
} else {
return pushTextInstanceImpl(target, text, renderState, textEmbedded);
}
}
export function pushSegmentFinale(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
lastPushedText: boolean,
textEmbedded: boolean,
): void {
if (renderState.generateStaticMarkup) {
return;
} else {
return pushSegmentFinaleImpl(
target,
renderState,
lastPushedText,
textEmbedded,
);
}
}
export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
if (renderState.generateStaticMarkup) {
// A completed boundary is done and doesn't need a representation in the HTML
// if we're not going to be hydrating it.
return;
}
pushStartActivityBoundaryImpl(target, renderState);
}
export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
if (renderState.generateStaticMarkup) {
return;
}
pushEndActivityBoundaryImpl(target, renderState);
}
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
if (renderState.generateStaticMarkup) {
// A completed boundary is done and doesn't need a representation in the HTML
// if we're not going to be hydrating it.
return true;
}
return writeStartCompletedSuspenseBoundaryImpl(destination, renderState);
}
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 {
if (renderState.generateStaticMarkup) {
// A client rendered boundary is done and doesn't need a representation in the HTML
// since we'll never hydrate it. This is arguably an error in static generation.
return true;
}
return writeStartClientRenderedSuspenseBoundaryImpl(
destination,
renderState,
errorDigest,
errorMessage,
errorStack,
errorComponentStack,
);
}
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
if (renderState.generateStaticMarkup) {
return true;
}
return writeEndCompletedSuspenseBoundaryImpl(destination, renderState);
}
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
if (renderState.generateStaticMarkup) {
return true;
}
return writeEndClientRenderedSuspenseBoundaryImpl(destination, renderState);
}
export function writePreambleStart(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
skipBlockingShell: boolean,
): void {
return writePreambleStartImpl(
destination,
resumableState,
renderState,
true, // skipBlockingShell
);
}
export type TransitionStatus = FormStatus;
export const NotPendingTransition: TransitionStatus = NotPending;