mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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.
330 lines
9.5 KiB
JavaScript
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;
|