Skip to content

Commit dcd26ee

Browse files
gnoffAndyPengc12
authored andcommitted
[Fizz][Float] Refactor Resources (facebook#27400)
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.
1 parent 813e43f commit dcd26ee

11 files changed

+1710
-689
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 823 additions & 666 deletions
Large diffs are not rendered by default.

packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
* @flow
88
*/
99

10-
import type {ResumableState, BoundaryResources} from './ReactFizzConfigDOM';
10+
import type {
11+
ResumableState,
12+
BoundaryResources,
13+
StyleQueue,
14+
Resource,
15+
} from './ReactFizzConfigDOM';
1116

1217
import {
1318
createRenderState as createRenderStateImpl,
@@ -46,16 +51,20 @@ export type RenderState = {
4651
importMapChunks: Array<Chunk | PrecomputedChunk>,
4752
preloadChunks: Array<Chunk | PrecomputedChunk>,
4853
hoistableChunks: Array<Chunk | PrecomputedChunk>,
49-
preconnects: Set<any>,
50-
fontPreloads: Set<any>,
51-
highImagePreloads: Set<any>,
52-
// usedImagePreloads: Set<any>,
53-
precedences: Map<string, Map<any, any>>,
54-
stylePrecedences: Map<string, any>,
55-
bootstrapScripts: Set<any>,
56-
scripts: Set<any>,
57-
bulkPreloads: Set<any>,
58-
preloadsMap: Map<string, any>,
54+
preconnects: Set<Resource>,
55+
fontPreloads: Set<Resource>,
56+
highImagePreloads: Set<Resource>,
57+
// usedImagePreloads: Set<Resource>,
58+
styles: Map<string, StyleQueue>,
59+
bootstrapScripts: Set<Resource>,
60+
scripts: Set<Resource>,
61+
bulkPreloads: Set<Resource>,
62+
preloads: {
63+
images: Map<string, Resource>,
64+
stylesheets: Map<string, Resource>,
65+
scripts: Map<string, Resource>,
66+
moduleScripts: Map<string, Resource>,
67+
},
5968
boundaryResources: ?BoundaryResources,
6069
stylesToHoist: boolean,
6170
// This is an extra field for the legacy renderer
@@ -94,12 +103,11 @@ export function createRenderState(
94103
fontPreloads: renderState.fontPreloads,
95104
highImagePreloads: renderState.highImagePreloads,
96105
// usedImagePreloads: renderState.usedImagePreloads,
97-
precedences: renderState.precedences,
98-
stylePrecedences: renderState.stylePrecedences,
106+
styles: renderState.styles,
99107
bootstrapScripts: renderState.bootstrapScripts,
100108
scripts: renderState.scripts,
101109
bulkPreloads: renderState.bulkPreloads,
102-
preloadsMap: renderState.preloadsMap,
110+
preloads: renderState.preloads,
103111
boundaryResources: renderState.boundaryResources,
104112
stylesToHoist: renderState.stylesToHoist,
105113

packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => {
8484
);
8585
const result = await readResult(stream);
8686
expect(result).toMatchInlineSnapshot(
87-
`"<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>"`,
87+
`"<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>"`,
8888
);
8989
});
9090

@@ -505,7 +505,7 @@ describe('ReactDOMFizzServerBrowser', () => {
505505
);
506506
const result = await readResult(stream);
507507
expect(result).toMatchInlineSnapshot(
508-
`"<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>"`,
508+
`"<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>"`,
509509
);
510510
});
511511

packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => {
9898
pipe(writable);
9999
jest.runAllTimers();
100100
expect(output.result).toMatchInlineSnapshot(
101-
`"<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>"`,
101+
`"<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>"`,
102102
);
103103
});
104104

packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
145145
});
146146
const prelude = await readContent(result.prelude);
147147
expect(prelude).toMatchInlineSnapshot(
148-
`"<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>"`,
148+
`"<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>"`,
149149
);
150150
});
151151

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
import {
13+
getVisibleChildren,
14+
insertNodesAndExecuteScripts,
15+
} from '../test-utils/FizzTestUtils';
16+
17+
// Polyfills for test environment
18+
global.ReadableStream =
19+
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
20+
global.TextEncoder = require('util').TextEncoder;
21+
22+
let React;
23+
let ReactDOM;
24+
let ReactDOMFizzServer;
25+
let ReactDOMFizzStatic;
26+
let Suspense;
27+
let container;
28+
29+
describe('ReactDOMFizzStaticFloat', () => {
30+
beforeEach(() => {
31+
jest.resetModules();
32+
React = require('react');
33+
ReactDOM = require('react-dom');
34+
ReactDOMFizzServer = require('react-dom/server.browser');
35+
if (__EXPERIMENTAL__) {
36+
ReactDOMFizzStatic = require('react-dom/static.browser');
37+
}
38+
Suspense = React.Suspense;
39+
container = document.createElement('div');
40+
document.body.appendChild(container);
41+
});
42+
43+
afterEach(() => {
44+
document.body.removeChild(container);
45+
});
46+
47+
async function readIntoContainer(stream) {
48+
const reader = stream.getReader();
49+
let result = '';
50+
while (true) {
51+
const {done, value} = await reader.read();
52+
if (done) {
53+
break;
54+
}
55+
result += Buffer.from(value).toString('utf8');
56+
}
57+
const temp = document.createElement('div');
58+
temp.innerHTML = result;
59+
await insertNodesAndExecuteScripts(temp, container, null);
60+
}
61+
62+
// @gate enablePostpone
63+
it('should transfer connection credentials across prerender and resume for stylesheets, scripts, and moduleScripts', async () => {
64+
let prerendering = true;
65+
function Postpone() {
66+
if (prerendering) {
67+
React.unstable_postpone();
68+
}
69+
return (
70+
<>
71+
<link rel="stylesheet" href="style creds" precedence="default" />
72+
<script async={true} src="script creds" data-meaningful="" />
73+
<script
74+
type="module"
75+
async={true}
76+
src="module creds"
77+
data-meaningful=""
78+
/>
79+
<link rel="stylesheet" href="style anon" precedence="default" />
80+
<script async={true} src="script anon" data-meaningful="" />
81+
<script
82+
type="module"
83+
async={true}
84+
src="module default"
85+
data-meaningful=""
86+
/>
87+
</>
88+
);
89+
}
90+
91+
function App() {
92+
ReactDOM.preload('style creds', {
93+
as: 'style',
94+
crossOrigin: 'use-credentials',
95+
});
96+
ReactDOM.preload('script creds', {
97+
as: 'script',
98+
crossOrigin: 'use-credentials',
99+
integrity: 'script-hash',
100+
});
101+
ReactDOM.preloadModule('module creds', {
102+
crossOrigin: 'use-credentials',
103+
integrity: 'module-hash',
104+
});
105+
ReactDOM.preload('style anon', {
106+
as: 'style',
107+
crossOrigin: 'anonymous',
108+
});
109+
ReactDOM.preload('script anon', {
110+
as: 'script',
111+
crossOrigin: 'foobar',
112+
});
113+
ReactDOM.preloadModule('module default', {
114+
integrity: 'module-hash',
115+
});
116+
return (
117+
<div>
118+
<Suspense fallback="Loading...">
119+
<Postpone />
120+
</Suspense>
121+
</div>
122+
);
123+
}
124+
125+
jest.mock('script creds', () => {}, {
126+
virtual: true,
127+
});
128+
jest.mock('module creds', () => {}, {
129+
virtual: true,
130+
});
131+
jest.mock('script anon', () => {}, {
132+
virtual: true,
133+
});
134+
jest.mock('module default', () => {}, {
135+
virtual: true,
136+
});
137+
138+
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
139+
expect(prerendered.postponed).not.toBe(null);
140+
141+
await readIntoContainer(prerendered.prelude);
142+
143+
expect(getVisibleChildren(container)).toEqual([
144+
<link
145+
rel="preload"
146+
as="style"
147+
href="style creds"
148+
crossorigin="use-credentials"
149+
/>,
150+
<link
151+
rel="preload"
152+
as="script"
153+
href="script creds"
154+
crossorigin="use-credentials"
155+
integrity="script-hash"
156+
/>,
157+
<link
158+
rel="modulepreload"
159+
href="module creds"
160+
crossorigin="use-credentials"
161+
integrity="module-hash"
162+
/>,
163+
<link rel="preload" as="style" href="style anon" crossorigin="" />,
164+
<link rel="preload" as="script" href="script anon" crossorigin="" />,
165+
<link
166+
rel="modulepreload"
167+
href="module default"
168+
integrity="module-hash"
169+
/>,
170+
<div>Loading...</div>,
171+
]);
172+
173+
prerendering = false;
174+
const content = await ReactDOMFizzServer.resume(
175+
<App />,
176+
JSON.parse(JSON.stringify(prerendered.postponed)),
177+
);
178+
179+
await readIntoContainer(content);
180+
181+
// Dispatch load event to injected stylesheet
182+
const linkCreds = document.querySelector(
183+
'link[rel="stylesheet"][href="style creds"]',
184+
);
185+
const linkAnon = document.querySelector(
186+
'link[rel="stylesheet"][href="style anon"]',
187+
);
188+
const event = document.createEvent('Events');
189+
event.initEvent('load', true, true);
190+
linkCreds.dispatchEvent(event);
191+
linkAnon.dispatchEvent(event);
192+
193+
// Wait for the instruction microtasks to flush.
194+
await 0;
195+
await 0;
196+
197+
expect(getVisibleChildren(document)).toEqual(
198+
<html>
199+
<head>
200+
<link
201+
rel="stylesheet"
202+
data-precedence="default"
203+
href="style creds"
204+
crossorigin="use-credentials"
205+
/>
206+
<link
207+
rel="stylesheet"
208+
data-precedence="default"
209+
href="style anon"
210+
crossorigin=""
211+
/>
212+
</head>
213+
<body>
214+
<div>
215+
<link
216+
rel="preload"
217+
as="style"
218+
href="style creds"
219+
crossorigin="use-credentials"
220+
/>
221+
<link
222+
rel="preload"
223+
as="script"
224+
href="script creds"
225+
crossorigin="use-credentials"
226+
integrity="script-hash"
227+
/>
228+
<link
229+
rel="modulepreload"
230+
href="module creds"
231+
crossorigin="use-credentials"
232+
integrity="module-hash"
233+
/>
234+
<link rel="preload" as="style" href="style anon" crossorigin="" />
235+
<link rel="preload" as="script" href="script anon" crossorigin="" />
236+
<link
237+
rel="modulepreload"
238+
href="module default"
239+
integrity="module-hash"
240+
/>
241+
<div />
242+
<script
243+
async=""
244+
src="script creds"
245+
crossorigin="use-credentials"
246+
integrity="script-hash"
247+
data-meaningful=""
248+
/>
249+
<script
250+
type="module"
251+
async=""
252+
src="module creds"
253+
crossorigin="use-credentials"
254+
integrity="module-hash"
255+
data-meaningful=""
256+
/>
257+
<script
258+
async=""
259+
src="script anon"
260+
crossorigin=""
261+
data-meaningful=""
262+
/>
263+
<script
264+
type="module"
265+
async=""
266+
src="module default"
267+
integrity="module-hash"
268+
data-meaningful=""
269+
/>
270+
</div>
271+
</body>
272+
</html>,
273+
);
274+
});
275+
});

packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => {
8686
);
8787
const prelude = await readContent(result.prelude);
8888
expect(prelude).toMatchInlineSnapshot(
89-
`"<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>"`,
89+
`"<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>"`,
9090
);
9191
});
9292

0 commit comments

Comments
 (0)