Skip to content

Commit

Permalink
[Fizz] Ensure Resumable State is Serializable (#27388)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sebmarkbage authored Sep 20, 2023
1 parent 1b1dcb8 commit b775564
Show file tree
Hide file tree
Showing 15 changed files with 575 additions and 340 deletions.
612 changes: 338 additions & 274 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Large diffs are not rendered by default.

35 changes: 33 additions & 2 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,

Expand Down
8 changes: 4 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
151 changes: 145 additions & 6 deletions packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ global.ReadableStream =
global.TextEncoder = require('util').TextEncoder;

let React;
let ReactDOM;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
Expand All @@ -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');
Expand Down Expand Up @@ -481,7 +483,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -523,7 +525,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -562,7 +564,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -610,7 +612,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -651,7 +653,7 @@ describe('ReactDOMFizzStaticBrowser', () => {

const resumed = await ReactDOMFizzServer.resume(
<App />,
prerendered.postponed,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

await readIntoContainer(prerendered.prelude);
Expand Down Expand Up @@ -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));
Expand All @@ -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>,
]);
});
});
12 changes: 6 additions & 6 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {
createResumableState,
createRenderState,
resumeRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';

Expand Down Expand Up @@ -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(
Expand All @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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),
Expand Down
Loading

0 comments on commit b775564

Please sign in to comment.