[Fizz] Ensure Resumable State is Serializable (#27388)

Moves writing queues to renderState.

We shouldn't need the resource tracking's value. We just need to know if
that resource has already been emitted. We can use a Set for this. To
ensure that set is directly serializable we can just use a
dictionary-like object with no value.

See individual commits for special cases.
This commit is contained in:
Sebastian Markbåge
2023-09-20 12:21:53 -04:00
committed by GitHub
parent 1b1dcb8a40
commit b775564d35
15 changed files with 575 additions and 340 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -39,11 +39,23 @@ export type RenderState = {
startInlineScript: PrecomputedChunk,
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
headChunks: null | Array<Chunk | PrecomputedChunk>,
externalRuntimeScript: null | any,
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
charsetChunks: Array<Chunk | PrecomputedChunk>,
preconnectChunks: Array<Chunk | PrecomputedChunk>,
importMapChunks: Array<Chunk | PrecomputedChunk>,
preloadChunks: Array<Chunk | PrecomputedChunk>,
hoistableChunks: Array<Chunk | PrecomputedChunk>,
preconnects: Set<any>,
fontPreloads: Set<any>,
highImagePreloads: Set<any>,
// usedImagePreloads: Set<any>,
precedences: Map<string, Map<any, any>>,
stylePrecedences: Map<string, any>,
bootstrapScripts: Set<any>,
scripts: Set<any>,
bulkPreloads: Set<any>,
preloadsMap: Map<string, any>,
boundaryResources: ?BoundaryResources,
stylesToHoist: boolean,
// This is an extra field for the legacy renderer
@@ -52,10 +64,17 @@ export type RenderState = {
export function createRenderState(
resumableState: ResumableState,
nonce: string | void,
generateStaticMarkup: boolean,
): RenderState {
const renderState = createRenderStateImpl(resumableState, nonce);
const renderState = createRenderStateImpl(
resumableState,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
return {
// Keep this in sync with ReactFizzConfigDOM
placeholderPrefix: renderState.placeholderPrefix,
@@ -64,11 +83,23 @@ export function createRenderState(
startInlineScript: renderState.startInlineScript,
htmlChunks: renderState.htmlChunks,
headChunks: renderState.headChunks,
externalRuntimeScript: renderState.externalRuntimeScript,
bootstrapChunks: renderState.bootstrapChunks,
charsetChunks: renderState.charsetChunks,
preconnectChunks: renderState.preconnectChunks,
importMapChunks: renderState.importMapChunks,
preloadChunks: renderState.preloadChunks,
hoistableChunks: renderState.hoistableChunks,
preconnects: renderState.preconnects,
fontPreloads: renderState.fontPreloads,
highImagePreloads: renderState.highImagePreloads,
// usedImagePreloads: renderState.usedImagePreloads,
precedences: renderState.precedences,
stylePrecedences: renderState.stylePrecedences,
bootstrapScripts: renderState.bootstrapScripts,
scripts: renderState.scripts,
bulkPreloads: renderState.bulkPreloads,
preloadsMap: renderState.preloadsMap,
boundaryResources: renderState.boundaryResources,
stylesToHoist: renderState.stylesToHoist,

View File

@@ -6339,7 +6339,7 @@ describe('ReactDOMFizzServer', () => {
const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
// Create a separate stream so it doesn't close the writable. I.e. simple concat.
@@ -6431,7 +6431,7 @@ describe('ReactDOMFizzServer', () => {
const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
ssrErrors.push(x.message);
@@ -6574,7 +6574,7 @@ describe('ReactDOMFizzServer', () => {
const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
ssrErrors.push(x.message);
@@ -6729,7 +6729,7 @@ describe('ReactDOMFizzServer', () => {
const resumed = ReactDOMFizzServer.resumeToPipeableStream(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
{
onError(x) {
ssrErrors.push(x.message);

View File

@@ -20,6 +20,7 @@ global.ReadableStream =
global.TextEncoder = require('util').TextEncoder;
let React;
let ReactDOM;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
@@ -29,6 +30,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMFizzServer = require('react-dom/server.browser');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static.browser');
@@ -481,7 +483,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
@@ -523,7 +525,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
@@ -562,7 +564,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
@@ -610,7 +612,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
@@ -651,7 +653,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(prerendered.prelude);
@@ -692,7 +694,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
const content = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
const html = await readContent(concat(prerendered.prelude, content));
@@ -701,4 +703,141 @@ describe('ReactDOMFizzStaticBrowser', () => {
expect(Array.from(html.matchAll(htmlEndTags)).length).toBe(1);
expect(Array.from(html.matchAll(bodyEndTags)).length).toBe(1);
});
// @gate enablePostpone
it('can prerender various hoistables and deduped resources', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return (
<>
<link rel="stylesheet" href="my-style2" precedence="low" />
<link rel="stylesheet" href="my-style1" precedence="high" />
<style precedence="high" href="my-style3">
style
</style>
<img src="my-img" />
</>
);
}
function App() {
ReactDOM.preconnect('example.com');
ReactDOM.preload('my-font', {as: 'font', type: 'font/woff2'});
ReactDOM.preload('my-style0', {as: 'style'});
// This should transfer the props in to the style that loads later.
ReactDOM.preload('my-style2', {
as: 'style',
crossOrigin: 'use-credentials',
});
return (
<div>
<Suspense fallback="Loading...">
<link rel="stylesheet" href="my-style1" precedence="high" />
<img src="my-img" />
<Postpone />
</Suspense>
<title>Hello World</title>
</div>
);
}
let calledInit = false;
jest.mock(
'init.js',
() => {
calledInit = true;
},
{virtual: true},
);
const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
bootstrapScripts: ['init.js'],
});
expect(prerendered.postponed).not.toBe(null);
await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual([
<link href="example.com" rel="preconnect" />,
<link
as="font"
crossorigin=""
href="my-font"
rel="preload"
type="font/woff2"
/>,
<link as="image" href="my-img" rel="preload" />,
<link data-precedence="high" href="my-style1" rel="stylesheet" />,
<link as="script" fetchpriority="low" href="init.js" rel="preload" />,
<link as="style" href="my-style0" rel="preload" />,
<link
as="style"
crossorigin="use-credentials"
href="my-style2"
rel="preload"
/>,
<title>Hello World</title>,
<div>Loading...</div>,
]);
prerendering = false;
const content = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);
await readIntoContainer(content);
expect(calledInit).toBe(true);
// Dispatch load event to injected stylesheet
const link = document.querySelector(
'link[rel="stylesheet"][href="my-style2"]',
);
const event = document.createEvent('Events');
event.initEvent('load', true, true);
link.dispatchEvent(event);
// Wait for the instruction microtasks to flush.
await 0;
await 0;
expect(getVisibleChildren(container)).toEqual([
<link href="example.com" rel="preconnect" />,
<link
as="font"
crossorigin=""
href="my-font"
rel="preload"
type="font/woff2"
/>,
<link as="image" href="my-img" rel="preload" />,
<link data-precedence="high" href="my-style1" rel="stylesheet" />,
<style data-href="my-style3" data-precedence="high">
style
</style>,
<link
crossorigin="use-credentials"
data-precedence="low"
href="my-style2"
rel="stylesheet"
/>,
<link as="script" fetchpriority="low" href="init.js" rel="preload" />,
<link as="style" href="my-style0" rel="preload" />,
<link
as="style"
crossorigin="use-credentials"
href="my-style2"
rel="preload"
/>,
<title>Hello World</title>,
<div>
<img src="my-img" />
<img src="my-img" />
</div>,
]);
});
});

View File

@@ -25,6 +25,7 @@ import {
import {
createResumableState,
createRenderState,
resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -96,10 +97,6 @@ function renderToReadableStream(
}
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createRequest(
@@ -108,6 +105,10 @@ function renderToReadableStream(
createRenderState(
resumableState,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
@@ -177,10 +178,9 @@ function resume(
const request = resumeRequest(
children,
postponedState,
createRenderState(
resumeRenderState(
postponedState.resumableState,
options ? options.nonce : undefined,
undefined, // importMap
),
options ? options.onError : undefined,
onAllReady,

View File

@@ -87,10 +87,6 @@ function renderToReadableStream(
}
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createRequest(
@@ -99,6 +95,10 @@ function renderToReadableStream(
createRenderState(
resumableState,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),

View File

@@ -25,6 +25,7 @@ import {
import {
createResumableState,
createRenderState,
resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -96,10 +97,6 @@ function renderToReadableStream(
}
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createRequest(
@@ -108,6 +105,10 @@ function renderToReadableStream(
createRenderState(
resumableState,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
@@ -177,10 +178,9 @@ function resume(
const request = resumeRequest(
children,
postponedState,
createRenderState(
resumeRenderState(
postponedState.resumableState,
options ? options.nonce : undefined,
undefined, // importMap
),
options ? options.onError : undefined,
onAllReady,

View File

@@ -27,6 +27,7 @@ import {
import {
createResumableState,
createRenderState,
resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -76,10 +77,6 @@ type PipeableStream = {
function createRequestImpl(children: ReactNodeList, options: void | Options) {
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
return createRequest(
@@ -88,6 +85,10 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
createRenderState(
resumableState,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
@@ -146,10 +147,9 @@ function resumeRequestImpl(
return resumeRequest(
children,
postponedState,
createRenderState(
resumeRenderState(
postponedState.resumableState,
options ? options.nonce : undefined,
undefined, // importMap
),
options ? options.onError : undefined,
options ? options.onAllReady : undefined,

View File

@@ -74,10 +74,6 @@ function prerender(
}
const resources = createResumableState(
options ? options.identifierPrefix : undefined,
undefined, // nonce is not compatible with prerendered bootstrap scripts
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createPrerenderRequest(
@@ -85,7 +81,11 @@ function prerender(
resources,
createRenderState(
resources,
undefined, // nonce
undefined, // nonce is not compatible with prerendered bootstrap scripts
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),

View File

@@ -74,10 +74,6 @@ function prerender(
}
const resources = createResumableState(
options ? options.identifierPrefix : undefined,
undefined, // nonce is not compatible with prerendered bootstrap scripts
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createPrerenderRequest(
@@ -85,7 +81,11 @@ function prerender(
resources,
createRenderState(
resources,
undefined, // nonce
undefined, // nonce is not compatible with prerendered bootstrap scripts
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),

View File

@@ -88,10 +88,6 @@ function prerenderToNodeStream(
}
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined, // nonce is not compatible with prerendered bootstrap scripts
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createPrerenderRequest(
@@ -99,7 +95,11 @@ function prerenderToNodeStream(
resumableState,
createRenderState(
resumableState,
undefined, // nonce
undefined, // nonce is not compatible with prerendered bootstrap scripts
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),

View File

@@ -63,15 +63,11 @@ function renderToStringImpl(
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
const request = createRequest(
children,
resumableState,
createRenderState(resumableState, undefined, generateStaticMarkup),
createRenderState(resumableState, generateStaticMarkup),
createRootFormatContext(),
Infinity,
onError,

View File

@@ -74,15 +74,11 @@ function renderToNodeStreamImpl(
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
const request = createRequest(
children,
resumableState,
createRenderState(resumableState, undefined, false),
createRenderState(resumableState, false),
createRootFormatContext(),
Infinity,
onError,

View File

@@ -52,16 +52,19 @@ function renderToStream(children: ReactNodeList, options: Options): Stream {
};
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createRequest(
children,
resumableState,
createRenderState(resumableState, undefined),
createRenderState(
resumableState,
undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
),
createRootFormatContext(undefined),
options ? options.progressiveChunkSize : undefined,
options.onError,

View File

@@ -3962,13 +3962,15 @@ function flushCompletedQueues(
destination,
request.resumableState,
request.renderState,
request.allPendingTasks === 0,
request.allPendingTasks === 0 &&
(request.trackedPostpones === null ||
request.trackedPostpones.workingMap.size === 0),
);
}
flushSegment(request, destination, completedRootSegment);
request.completedRootSegment = null;
writeCompletedRoot(destination, request.resumableState);
writeCompletedRoot(destination, request.renderState);
} else {
// We haven't flushed the root yet so we don't need to check any other branches further down
return;
@@ -4166,6 +4168,10 @@ export function getResumableState(request: Request): ResumableState {
return request.resumableState;
}
export function getRenderState(request: Request): RenderState {
return request.renderState;
}
function addToReplayParent(
node: ResumableNode,
parentKeyPath: Root | KeyNode,