[Fizz][Float] Refactor Resources (#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.
This commit is contained in:
Josh Story
2023-09-26 09:59:39 -07:00
committed by GitHub
parent bff6be8eb1
commit 49eba01930
11 changed files with 1717 additions and 696 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,12 @@
* @flow
*/
import type {ResumableState, BoundaryResources} from './ReactFizzConfigDOM';
import type {
ResumableState,
BoundaryResources,
StyleQueue,
Resource,
} from './ReactFizzConfigDOM';
import {
createRenderState as createRenderStateImpl,
@@ -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
@@ -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,

View File

@@ -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>"`,
);
});
@@ -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>"`,
);
});

View File

@@ -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>"`,
);
});

View File

@@ -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>"`,
);
});

View File

@@ -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>,
);
});
});

View File

@@ -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>"`,
);
});

View File

@@ -4014,6 +4014,87 @@ body {
);
});
it('can promote images to high priority when at least one instance specifies a high fetchPriority', async () => {
function App() {
// If a ends up in a higher priority queue than b it will flush first
ReactDOM.preload('a', {as: 'image'});
ReactDOM.preload('b', {as: 'image'});
return (
<html>
<body>
<link rel="stylesheet" href="foo" precedence="default" />
<img src="1" />
<img src="2" />
<img src="3" />
<img src="4" />
<img src="5" />
<img src="6" />
<img src="7" />
<img src="8" />
<img src="9" />
<img src="10" />
<img src="11" />
<img src="12" />
<img src="a" fetchPriority="low" />
<img src="a" />
<img src="a" fetchPriority="high" />
<img src="a" />
<img src="a" />
</body>
</html>
);
}
await act(() => {
renderToPipeableStream(<App />).pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
{/* The First 10 high priority images were just the first 10 rendered images */}
<link rel="preload" as="image" href="1" />
<link rel="preload" as="image" href="2" />
<link rel="preload" as="image" href="3" />
<link rel="preload" as="image" href="4" />
<link rel="preload" as="image" href="5" />
<link rel="preload" as="image" href="6" />
<link rel="preload" as="image" href="7" />
<link rel="preload" as="image" href="8" />
<link rel="preload" as="image" href="9" />
<link rel="preload" as="image" href="10" />
{/* The "a" image was rendered a few times but since at least one of those was with
fetchPriorty="high" it ends up in the high priority queue */}
<link rel="preload" as="image" href="a" />
{/* Stylesheets come in between high priority images and regular preloads */}
<link rel="stylesheet" href="foo" data-precedence="default" />
{/* The remainig images that preloaded at regular priority */}
<link rel="preload" as="image" href="b" />
<link rel="preload" as="image" href="11" />
<link rel="preload" as="image" href="12" />
</head>
<body>
<img src="1" />
<img src="2" />
<img src="3" />
<img src="4" />
<img src="5" />
<img src="6" />
<img src="7" />
<img src="8" />
<img src="9" />
<img src="10" />
<img src="11" />
<img src="12" />
<img src="a" fetchpriority="low" />
<img src="a" />
<img src="a" fetchpriority="high" />
<img src="a" />
<img src="a" />
</body>
</html>,
);
});
it('preloads from rendered images properly use srcSet and sizes', async () => {
function App() {
ReactDOM.preload('1', {as: 'image', imageSrcSet: 'ss1'});
@@ -4119,6 +4200,501 @@ body {
);
});
it('should warn if you preload a stylesheet and then render a style tag with the same href', async () => {
const style = 'body { color: red; }';
function App() {
ReactDOM.preload('foo', {as: 'style'});
return (
<html>
<body>
hello
<style precedence="default" href="foo">
{style}
</style>
</body>
</html>
);
}
await expect(async () => {
await act(() => {
renderToPipeableStream(<App />).pipe(writable);
});
}).toErrorDev([
'React encountered a hoistable style tag for the same href as a preload: "foo". When using a style tag to inline styles you should not also preload it as a stylsheet.',
]);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<style data-precedence="default" data-href="foo">
{style}
</style>
<link rel="preload" as="style" href="foo" />
</head>
<body>hello</body>
</html>,
);
});
it('should preload only once even if you discover a stylesheet, script, or moduleScript late', async () => {
function App() {
// We start with preinitializing some resources first
ReactDOM.preinit('shell preinit/shell', {as: 'style'});
ReactDOM.preinit('shell preinit/shell', {as: 'script'});
ReactDOM.preinitModule('shell preinit/shell', {as: 'script'});
// We initiate all the shell preloads
ReactDOM.preload('shell preinit/shell', {as: 'style'});
ReactDOM.preload('shell preinit/shell', {as: 'script'});
ReactDOM.preloadModule('shell preinit/shell', {as: 'script'});
ReactDOM.preload('shell/shell preinit', {as: 'style'});
ReactDOM.preload('shell/shell preinit', {as: 'script'});
ReactDOM.preloadModule('shell/shell preinit', {as: 'script'});
ReactDOM.preload('shell/shell render', {as: 'style'});
ReactDOM.preload('shell/shell render', {as: 'script'});
ReactDOM.preloadModule('shell/shell render');
ReactDOM.preload('shell/late preinit', {as: 'style'});
ReactDOM.preload('shell/late preinit', {as: 'script'});
ReactDOM.preloadModule('shell/late preinit');
ReactDOM.preload('shell/late render', {as: 'style'});
ReactDOM.preload('shell/late render', {as: 'script'});
ReactDOM.preloadModule('shell/late render');
// we preinit later ones that should be created by
ReactDOM.preinit('shell/shell preinit', {as: 'style'});
ReactDOM.preinit('shell/shell preinit', {as: 'script'});
ReactDOM.preinitModule('shell/shell preinit');
ReactDOM.preinit('late/shell preinit', {as: 'style'});
ReactDOM.preinit('late/shell preinit', {as: 'script'});
ReactDOM.preinitModule('late/shell preinit');
return (
<html>
<body>
<link
rel="stylesheet"
precedence="default"
href="shell/shell render"
/>
<script async={true} src="shell/shell render" />
<script type="module" async={true} src="shell/shell render" />
<link
rel="stylesheet"
precedence="default"
href="late/shell render"
/>
<script async={true} src="late/shell render" />
<script type="module" async={true} src="late/shell render" />
<Suspense fallback="late...">
<BlockedOn value="late">
<Late />
</BlockedOn>
</Suspense>
<Suspense fallback="later...">
<BlockedOn value="later">
<Later />
</BlockedOn>
</Suspense>
</body>
</html>
);
}
function Late() {
ReactDOM.preload('late/later preinit', {as: 'style'});
ReactDOM.preload('late/later preinit', {as: 'script'});
ReactDOM.preloadModule('late/later preinit');
ReactDOM.preload('late/later render', {as: 'style'});
ReactDOM.preload('late/later render', {as: 'script'});
ReactDOM.preloadModule('late/later render');
ReactDOM.preload('late/shell preinit', {as: 'style'});
ReactDOM.preload('late/shell preinit', {as: 'script'});
ReactDOM.preloadModule('late/shell preinit');
ReactDOM.preload('late/shell render', {as: 'style'});
ReactDOM.preload('late/shell render', {as: 'script'});
ReactDOM.preloadModule('late/shell render');
// late preinits don't actually flush so we won't see this in the DOM as a stylesehet but we should see
// the preload for this resource
ReactDOM.preinit('shell/late preinit', {as: 'style'});
ReactDOM.preinit('shell/late preinit', {as: 'script'});
ReactDOM.preinitModule('shell/late preinit');
return (
<>
Late
<link
rel="stylesheet"
precedence="default"
href="shell/late render"
/>
<script async={true} src="shell/late render" />
<script type="module" async={true} src="shell/late render" />
</>
);
}
function Later() {
// late preinits don't actually flush so we won't see this in the DOM as a stylesehet but we should see
// the preload for this resource
ReactDOM.preinit('late/later preinit', {as: 'style'});
ReactDOM.preinit('late/later preinit', {as: 'script'});
ReactDOM.preinitModule('late/later preinit');
return (
<>
Later
<link
rel="stylesheet"
precedence="default"
href="late/later render"
/>
<script async={true} src="late/later render" />
<script type="module" async={true} src="late/later render" />
</>
);
}
await act(() => {
renderToPipeableStream(<App />).pipe(writable);
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
data-precedence="default"
href="shell preinit/shell"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell render"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell render"
/>
<script async="" src="shell preinit/shell" />
<script async="" src="shell preinit/shell" type="module" />
<script async="" src="shell/shell preinit" />
<script async="" src="shell/shell preinit" type="module" />
<script async="" src="late/shell preinit" />
<script async="" src="late/shell preinit" type="module" />
<script async="" src="shell/shell render" />
<script async="" src="shell/shell render" type="module" />
<script async="" src="late/shell render" />
<script async="" src="late/shell render" type="module" />
<link rel="preload" as="style" href="shell/late preinit" />
<link rel="preload" as="script" href="shell/late preinit" />
<link rel="modulepreload" href="shell/late preinit" />
<link rel="preload" as="style" href="shell/late render" />
<link rel="preload" as="script" href="shell/late render" />
<link rel="modulepreload" href="shell/late render" />
</head>
<body>
{'late...'}
{'later...'}
</body>
</html>,
);
await act(() => {
resolveText('late');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
data-precedence="default"
href="shell preinit/shell"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell render"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell render"
/>
{/* FROM HERE */}
<link
rel="stylesheet"
data-precedence="default"
href="shell/late render"
/>
{/** TO HERE:
* This was hoisted by boundary complete instruction. The preload was already emitted in the
* shell but we see it below because this was inserted clientside by precedence.
* We don't observe the "shell/late preinit" because these do not flush unless they are flushing
* with the shell
* */}
<script async="" src="shell preinit/shell" />
<script async="" src="shell preinit/shell" type="module" />
<script async="" src="shell/shell preinit" />
<script async="" src="shell/shell preinit" type="module" />
<script async="" src="late/shell preinit" />
<script async="" src="late/shell preinit" type="module" />
<script async="" src="shell/shell render" />
<script async="" src="shell/shell render" type="module" />
<script async="" src="late/shell render" />
<script async="" src="late/shell render" type="module" />
<link rel="preload" as="style" href="shell/late preinit" />
<link rel="preload" as="script" href="shell/late preinit" />
<link rel="modulepreload" href="shell/late preinit" />
<link rel="preload" as="style" href="shell/late render" />
<link rel="preload" as="script" href="shell/late render" />
<link rel="modulepreload" href="shell/late render" />
</head>
<body>
{'late...'}
{'later...'}
{/* FROM HERE */}
<script async="" src="shell/late preinit" />
<script async="" src="shell/late preinit" type="module" />
<script async="" src="shell/late render" />
<script async="" src="shell/late render" type="module" />
<link rel="preload" as="style" href="late/later preinit" />
<link rel="preload" as="script" href="late/later preinit" />
<link rel="modulepreload" href="late/later preinit" />
<link rel="preload" as="style" href="late/later render" />
<link rel="preload" as="script" href="late/later render" />
<link rel="modulepreload" href="late/later render" />
{/** TO HERE:
* These resources streamed into the body during the boundary flush. Scripts go first then
* preloads according to our streaming queue priorities. Note also that late/shell resources
* where the resource already emitted in the shell and the preload is invoked later do not
* end up with a preload in the document at all.
* */}
</body>
</html>,
);
await act(() => {
resolveText('later');
});
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
data-precedence="default"
href="shell preinit/shell"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell render"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell render"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/late render"
/>
{/* FROM HERE */}
<link
rel="stylesheet"
data-precedence="default"
href="late/later render"
/>
{/** TO HERE:
* This was hoisted by boundary complete instruction. The preload was already emitted in the
* shell but we see it below because this was inserted clientside by precedence
* We don't observe the "late/later preinit" because these do not flush unless they are flushing
* with the shell
* */}
<script async="" src="shell preinit/shell" />
<script async="" src="shell preinit/shell" type="module" />
<script async="" src="shell/shell preinit" />
<script async="" src="shell/shell preinit" type="module" />
<script async="" src="late/shell preinit" />
<script async="" src="late/shell preinit" type="module" />
<script async="" src="shell/shell render" />
<script async="" src="shell/shell render" type="module" />
<script async="" src="late/shell render" />
<script async="" src="late/shell render" type="module" />
<link rel="preload" as="style" href="shell/late preinit" />
<link rel="preload" as="script" href="shell/late preinit" />
<link rel="modulepreload" href="shell/late preinit" />
<link rel="preload" as="style" href="shell/late render" />
<link rel="preload" as="script" href="shell/late render" />
<link rel="modulepreload" href="shell/late render" />
</head>
<body>
{'late...'}
{'later...'}
<script async="" src="shell/late preinit" />
<script async="" src="shell/late preinit" type="module" />
<script async="" src="shell/late render" />
<script async="" src="shell/late render" type="module" />
<link rel="preload" as="style" href="late/later preinit" />
<link rel="preload" as="script" href="late/later preinit" />
<link rel="modulepreload" href="late/later preinit" />
<link rel="preload" as="style" href="late/later render" />
<link rel="preload" as="script" href="late/later render" />
<link rel="modulepreload" href="late/later render" />
{/* FROM HERE */}
<script async="" src="late/later preinit" />
<script async="" src="late/later preinit" type="module" />
<script async="" src="late/later render" />
<script async="" src="late/later render" type="module" />
{/** TO HERE:
* These resources streamed into the body during the boundary flush. Scripts go first then
* preloads according to our streaming queue priorities
* */}
</body>
</html>,
);
loadStylesheets();
assertLog([
'load stylesheet: shell preinit/shell',
'load stylesheet: shell/shell preinit',
'load stylesheet: late/shell preinit',
'load stylesheet: shell/shell render',
'load stylesheet: late/shell render',
'load stylesheet: shell/late render',
'load stylesheet: late/later render',
]);
ReactDOMClient.hydrateRoot(document, <App />);
await waitForAll([]);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link
rel="stylesheet"
data-precedence="default"
href="shell preinit/shell"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/shell render"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/shell render"
/>
<link
rel="stylesheet"
data-precedence="default"
href="shell/late render"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/later render"
/>
{/* FROM HERE */}
<link
rel="stylesheet"
data-precedence="default"
href="shell/late preinit"
/>
<link
rel="stylesheet"
data-precedence="default"
href="late/later preinit"
/>
{/** TO HERE:
* The client render patches in the two missing preinit stylesheets when hydration happens
* Note that this is only because we repeated the calls to preinit on the client
* */}
<script async="" src="shell preinit/shell" />
<script async="" src="shell preinit/shell" type="module" />
<script async="" src="shell/shell preinit" />
<script async="" src="shell/shell preinit" type="module" />
<script async="" src="late/shell preinit" />
<script async="" src="late/shell preinit" type="module" />
<script async="" src="shell/shell render" />
<script async="" src="shell/shell render" type="module" />
<script async="" src="late/shell render" />
<script async="" src="late/shell render" type="module" />
<link rel="preload" as="style" href="shell/late preinit" />
<link rel="preload" as="script" href="shell/late preinit" />
<link rel="modulepreload" href="shell/late preinit" />
<link rel="preload" as="style" href="shell/late render" />
<link rel="preload" as="script" href="shell/late render" />
<link rel="modulepreload" href="shell/late render" />
</head>
<body>
{'Late'}
{'Later'}
<script async="" src="shell/late preinit" />
<script async="" src="shell/late preinit" type="module" />
<script async="" src="shell/late render" />
<script async="" src="shell/late render" type="module" />
<link rel="preload" as="style" href="late/later preinit" />
<link rel="preload" as="script" href="late/later preinit" />
<link rel="modulepreload" href="late/later preinit" />
<link rel="preload" as="style" href="late/later render" />
<link rel="preload" as="script" href="late/later render" />
<link rel="modulepreload" href="late/later render" />
<script async="" src="late/later preinit" />
<script async="" src="late/later preinit" type="module" />
<script async="" src="late/later render" />
<script async="" src="late/later render" type="module" />
</body>
</html>,
);
});
describe('ReactDOM.prefetchDNS(href)', () => {
it('creates a dns-prefetch resource when called', async () => {
function App({url}) {

View File

@@ -24,6 +24,7 @@ export type PreloadModuleOptions = {
as?: string,
crossOrigin?: string,
integrity?: string,
nonce?: string,
};
export type PreinitOptions = {
as: string,
@@ -37,6 +38,7 @@ export type PreinitModuleOptions = {
as?: string,
crossOrigin?: string,
integrity?: string,
nonce?: string,
};
export type CrossOriginEnum = '' | 'use-credentials';
@@ -56,6 +58,7 @@ export type PreloadModuleImplOptions = {
as?: ?string,
crossOrigin?: ?CrossOriginEnum,
integrity?: ?string,
nonce?: ?string,
};
export type PreinitStyleOptions = {
crossOrigin?: ?string,
@@ -70,7 +73,8 @@ export type PreinitScriptOptions = {
};
export type PreinitModuleScriptOptions = {
crossOrigin?: ?CrossOriginEnum,
integrity?: string,
integrity?: ?string,
nonce?: ?string,
};
export type HostDispatcher = {

View File

@@ -93,6 +93,7 @@ async function executeScript(script: Element) {
'You must set the current document to the global document to use script src in tests',
);
}
try {
// $FlowFixMe
require(scriptSrc);
@@ -177,8 +178,8 @@ function getVisibleChildren(element: Element): React$Node {
while (node) {
if (node.nodeType === 1) {
if (
node.tagName !== 'SCRIPT' &&
node.tagName !== 'script' &&
((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
node.hasAttribute('data-meaningful')) &&
node.tagName !== 'TEMPLATE' &&
node.tagName !== 'template' &&
!node.hasAttribute('hidden') &&

View File

@@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => {
});
const result = 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>"`,
);
});