Skip to content

Commit

Permalink
[Fizz][Float] Refactor Resources (#27400)
Browse files Browse the repository at this point in the history
Refactors Resources to have a more compact and memory efficient
struture. Resources generally are just an Array of chunks. A resource is
flushed when it's chunks is length zero. A resource does not have any
other state.

Stylesheets and Style tags are different and have been modeled as a unit
as a StyleQueue. This object stores the style rules to flush as part of
style tags using precedence as well as all the stylesheets associated
with the precedence. Stylesheets still need to track state because it
affects how we issue boundary completion instructions. Additionally
stylesheets encode chunks lazily because we may never write them as html
if they are discovered late.

The preload props transfer is now maximally compact (only stores the
props we would ever actually adopt) and only stores props for
stylesheets and scripts because other preloads have no resource
counterpart to adopt props into. The ResumableState maps that track
which keys have been observed are being overloaded. Previously if a key
was found it meant that a resource already exists (either in this render
or in a prior prerender). Now we discriminate between null and object
values. If map value is null we can assume the resource exists but if it
is an object that represents a prior preload for that resource and the
resource must still be constructed.
  • Loading branch information
gnoff authored Sep 26, 2023
1 parent bff6be8 commit 49eba01
Show file tree
Hide file tree
Showing 11 changed files with 1,710 additions and 689 deletions.
1,489 changes: 823 additions & 666 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Large diffs are not rendered by default.

36 changes: 22 additions & 14 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
* @flow
*/

import type {ResumableState, BoundaryResources} from './ReactFizzConfigDOM';
import type {
ResumableState,
BoundaryResources,
StyleQueue,
Resource,
} from './ReactFizzConfigDOM';

import {
createRenderState as createRenderStateImpl,
Expand Down Expand Up @@ -46,16 +51,20 @@ export type RenderState = {
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>,
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>,
},
boundaryResources: ?BoundaryResources,
stylesToHoist: boolean,
// This is an extra field for the legacy renderer
Expand Down Expand Up @@ -94,12 +103,11 @@ export function createRenderState(
fontPreloads: renderState.fontPreloads,
highImagePreloads: renderState.highImagePreloads,
// usedImagePreloads: renderState.usedImagePreloads,
precedences: renderState.precedences,
stylePrecedences: renderState.stylePrecedences,
styles: renderState.styles,
bootstrapScripts: renderState.bootstrapScripts,
scripts: renderState.scripts,
bulkPreloads: renderState.bulkPreloads,
preloadsMap: renderState.preloadsMap,
preloads: renderState.preloads,
boundaryResources: renderState.boundaryResources,
stylesToHoist: renderState.stylesToHoist,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down Expand Up @@ -505,7 +505,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script" fetchPriority="low" nonce="R4nd0m"/><link rel="modulepreload" href="init.mjs" fetchPriority="low" nonce="R4nd0m"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" nonce="R4nd0m" href="init.js"/><link rel="modulepreload" fetchPriority="low" nonce="R4nd0m" href="init.mjs"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => {
pipe(writable);
jest.runAllTimers();
expect(output.result).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
});
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down
275 changes: 275 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/**
* 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.
*
* @emails react-core
*/

'use strict';

import {
getVisibleChildren,
insertNodesAndExecuteScripts,
} from '../test-utils/FizzTestUtils';

// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;

let React;
let ReactDOM;
let ReactDOMFizzServer;
let ReactDOMFizzStatic;
let Suspense;
let container;

describe('ReactDOMFizzStaticFloat', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMFizzServer = require('react-dom/server.browser');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static.browser');
}
Suspense = React.Suspense;
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
});

async function readIntoContainer(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
result += Buffer.from(value).toString('utf8');
}
const temp = document.createElement('div');
temp.innerHTML = result;
await insertNodesAndExecuteScripts(temp, container, null);
}

// @gate enablePostpone
it('should transfer connection credentials across prerender and resume for stylesheets, scripts, and moduleScripts', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return (
<>
<link rel="stylesheet" href="style creds" precedence="default" />
<script async={true} src="script creds" data-meaningful="" />
<script
type="module"
async={true}
src="module creds"
data-meaningful=""
/>
<link rel="stylesheet" href="style anon" precedence="default" />
<script async={true} src="script anon" data-meaningful="" />
<script
type="module"
async={true}
src="module default"
data-meaningful=""
/>
</>
);
}

function App() {
ReactDOM.preload('style creds', {
as: 'style',
crossOrigin: 'use-credentials',
});
ReactDOM.preload('script creds', {
as: 'script',
crossOrigin: 'use-credentials',
integrity: 'script-hash',
});
ReactDOM.preloadModule('module creds', {
crossOrigin: 'use-credentials',
integrity: 'module-hash',
});
ReactDOM.preload('style anon', {
as: 'style',
crossOrigin: 'anonymous',
});
ReactDOM.preload('script anon', {
as: 'script',
crossOrigin: 'foobar',
});
ReactDOM.preloadModule('module default', {
integrity: 'module-hash',
});
return (
<div>
<Suspense fallback="Loading...">
<Postpone />
</Suspense>
</div>
);
}

jest.mock('script creds', () => {}, {
virtual: true,
});
jest.mock('module creds', () => {}, {
virtual: true,
});
jest.mock('script anon', () => {}, {
virtual: true,
});
jest.mock('module default', () => {}, {
virtual: true,
});

const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);

await readIntoContainer(prerendered.prelude);

expect(getVisibleChildren(container)).toEqual([
<link
rel="preload"
as="style"
href="style creds"
crossorigin="use-credentials"
/>,
<link
rel="preload"
as="script"
href="script creds"
crossorigin="use-credentials"
integrity="script-hash"
/>,
<link
rel="modulepreload"
href="module creds"
crossorigin="use-credentials"
integrity="module-hash"
/>,
<link rel="preload" as="style" href="style anon" crossorigin="" />,
<link rel="preload" as="script" href="script anon" crossorigin="" />,
<link
rel="modulepreload"
href="module default"
integrity="module-hash"
/>,
<div>Loading...</div>,
]);

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

await readIntoContainer(content);

// Dispatch load event to injected stylesheet
const linkCreds = document.querySelector(
'link[rel="stylesheet"][href="style creds"]',
);
const linkAnon = document.querySelector(
'link[rel="stylesheet"][href="style anon"]',
);
const event = document.createEvent('Events');
event.initEvent('load', true, true);
linkCreds.dispatchEvent(event);
linkAnon.dispatchEvent(event);

// Wait for the instruction microtasks to flush.
await 0;
await 0;

expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
data-precedence="default"
href="style creds"
crossorigin="use-credentials"
/>
<link
rel="stylesheet"
data-precedence="default"
href="style anon"
crossorigin=""
/>
</head>
<body>
<div>
<link
rel="preload"
as="style"
href="style creds"
crossorigin="use-credentials"
/>
<link
rel="preload"
as="script"
href="script creds"
crossorigin="use-credentials"
integrity="script-hash"
/>
<link
rel="modulepreload"
href="module creds"
crossorigin="use-credentials"
integrity="module-hash"
/>
<link rel="preload" as="style" href="style anon" crossorigin="" />
<link rel="preload" as="script" href="script anon" crossorigin="" />
<link
rel="modulepreload"
href="module default"
integrity="module-hash"
/>
<div />
<script
async=""
src="script creds"
crossorigin="use-credentials"
integrity="script-hash"
data-meaningful=""
/>
<script
type="module"
async=""
src="module creds"
crossorigin="use-credentials"
integrity="module-hash"
data-meaningful=""
/>
<script
async=""
src="script anon"
crossorigin=""
data-meaningful=""
/>
<script
type="module"
async=""
src="module default"
integrity="module-hash"
data-meaningful=""
/>
</div>
</body>
</html>,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => {
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
);
});

Expand Down
Loading

0 comments on commit 49eba01

Please sign in to comment.