[Fizz] preload `bootstrapModules` (#26754)
stacked on #26753 Adds support for preloading bootstrapModules. We don't yet support modules in Float's public interface but this implementation should be compatible with what we do when we add it.
Josh Story committed
May 31, 2023 at 16:48 UTC
ae31d2ea3c3f9f0a87ff2c6193484d5d8786bc5f
7 files changed
+84
-18
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+54
-8
@@ -296,6 +296,8 @@ export function createResponseState(
296
const integrity =
297
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
298
299
+ preloadBootstrapModule(resources, src, nonce, integrity);
300
+
301
bootstrapChunks.push(
302
startModuleSrc,
303
stringToChunk(escapeTextForBrowser(src)),
@@ -1977,7 +1979,7 @@ function pushLink(
1979
}
1980
}
1981
pushLinkImpl(resource.chunks, resource.props);
1980
- resources.usedStylesheets.add(resource);
1982
+ resources.usedStylesheets.set(key, resource);
1983
return pushLinkImpl(target, props);
1984
} else {
1985
// This stylesheet refers to a Resource and we create a new one if necessary
@@ -4249,8 +4251,7 @@ export function writePreamble(
4251
// Flush unblocked stylesheets by precedence
4252
resources.precedences.forEach(flushAllStylesInPreamble, destination);
4253
4252
- resources.usedStylesheets.forEach(resource => {
4253
- const key = getResourceKey(resource.props.as, resource.props.href);
4254
+ resources.usedStylesheets.forEach((resource, key) => {
4255
if (resources.stylesMap.has(key)) {
4256
// The underlying stylesheet is represented both as a used stylesheet
4257
// (a regular component we will attempt to preload) and as a StylesheetResource.
@@ -4345,8 +4346,7 @@ export function writeHoistables(
4346
// but we want to kick off preloading as soon as possible
4347
resources.precedences.forEach(preloadLateStyles, destination);
4348
4348
- resources.usedStylesheets.forEach(resource => {
4349
- const key = getResourceKey(resource.props.as, resource.props.href);
4349
+ resources.usedStylesheets.forEach((resource, key) => {
4350
if (resources.stylesMap.has(key)) {
4351
// The underlying stylesheet is represented both as a used stylesheet
4352
// (a regular component we will attempt to preload) and as a StylesheetResource.
@@ -4861,12 +4861,18 @@ type PreconnectProps = {
4861
};
4862
type PreconnectResource = TResource<'preconnect', null>;
4863
4864
-type PreloadProps = {
4864
+type PreloadAsProps = {
4865
rel: 'preload',
4866
as: string,
4867
href: string,
4868
[string]: mixed,
4869
};
4870
+type PreloadModuleProps = {
4871
+ rel: 'modulepreload',
4872
+ href: string,
4873
+ [string]: mixed,
4874
+};
4875
+type PreloadProps = PreloadAsProps | PreloadModuleProps;
4876
type PreloadResource = TResource<'preload', PreloadProps>;
4877
4878
type StylesheetProps = {
@@ -4911,7 +4917,7 @@ export type Resources = {
4917
// usedImagePreloads: Set<PreloadResource>,
4918
precedences: Map<string, Set<StyleResource>>,
4919
stylePrecedences: Map<string, StyleTagResource>,
4914
- usedStylesheets: Set<PreloadResource>,
4920
+ usedStylesheets: Map<string, PreloadResource>,
4921
scripts: Set<ScriptResource>,
4922
usedScripts: Set<PreloadResource>,
4923
explicitStylesheetPreloads: Set<PreloadResource>,
@@ -4939,7 +4945,7 @@ export function createResources(): Resources {
4945
// usedImagePreloads: new Set(),
4946
precedences: new Map(),
4947
stylePrecedences: new Map(),
4942
- usedStylesheets: new Set(),
4948
+ usedStylesheets: new Map(),
4949
scripts: new Set(),
4950
usedScripts: new Set(),
4951
explicitStylesheetPreloads: new Set(),
@@ -5512,6 +5518,46 @@ function preloadBootstrapScript(
5518
pushLinkImpl(resource.chunks, props);
5519
}
5520
5521
+// This function is only safe to call at Request start time since it assumes
5522
+// that each module has not already been preloaded. If we find a need to preload
5523
+// scripts at any other point in time we will need to check whether the preload
5524
+// already exists and not assume it
5525
+function preloadBootstrapModule(
5526
+ resources: Resources,
5527
+ src: string,
5528
+ nonce: ?string,
5529
+ integrity: ?string,
5530
+): void {
5531
+ const key = getResourceKey('script', src);
5532
+ if (__DEV__) {
5533
+ if (resources.preloadsMap.has(key)) {
5534
+ // This is coded as a React error because it should be impossible for a userspace preload to preempt this call
5535
+ // If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
5536
+ // rather than instruct the user to not preload their bootstrap scripts themselves
5537
+ console.error(
5538
+ 'Internal React Error: React expected bootstrap module with src "%s" to not have been preloaded already. please file an issue',
5539
+ src,
5540
+ );
5541
+ }
5542
+ }
5543
+ const props: PreloadModuleProps = {
5544
+ rel: 'modulepreload',
5545
+ href: src,
5546
+ nonce,
5547
+ integrity,
5548
+ };
5549
+ const resource: PreloadResource = {
5550
+ type: 'preload',
5551
+ chunks: [],
5552
+ state: NoState,
5553
+ props,
5554
+ };
5555
+ resources.preloadsMap.set(key, resource);
5556
+ resources.explicitScriptPreloads.add(resource);
5557
+ pushLinkImpl(resource.chunks, props);
5558
+ return;
5559
+}
5560
+
5561
function internalPreinitScript(
5562
resources: Resources,
5563
src: string,
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+24
-4
@@ -600,7 +600,10 @@ describe('ReactDOMFizzServer', () => {
600
'init.js',
601
{src: 'init2.js', integrity: 'init2hash'},
602
],
603
- bootstrapModules: ['init.mjs'],
603
+ bootstrapModules: [
604
+ 'init.mjs',
605
+ {src: 'init2.mjs', integrity: 'init2hash'},
606
+ ],
607
},
608
);
609
pipe(writable);
@@ -615,16 +618,23 @@ describe('ReactDOMFizzServer', () => {
618
nonce={CSPnonce}
619
integrity="init2hash"
620
/>,
621
+ <link rel="modulepreload" href="init.mjs" nonce={CSPnonce} />,
622
+ <link
623
+ rel="modulepreload"
624
+ href="init2.mjs"
625
+ nonce={CSPnonce}
626
+ integrity="init2hash"
627
+ />,
628
<div>Loading...</div>,
629
]);
630
621
- // check that there are 4 scripts with a matching nonce:
622
- // The runtime script, an inline bootstrap script, and two src scripts
631
+ // check that there are 6 scripts with a matching nonce:
632
+ // The runtime script, an inline bootstrap script, two bootstrap scripts and two bootstrap modules
633
expect(
634
Array.from(container.getElementsByTagName('script')).filter(
635
node => node.getAttribute('nonce') === CSPnonce,
636
).length,
627
- ).toEqual(5);
637
+ ).toEqual(6);
638
639
await act(() => {
640
resolve({default: Text});
@@ -638,6 +648,13 @@ describe('ReactDOMFizzServer', () => {
648
nonce={CSPnonce}
649
integrity="init2hash"
650
/>,
651
+ <link rel="modulepreload" href="init.mjs" nonce={CSPnonce} />,
652
+ <link
653
+ rel="modulepreload"
654
+ href="init2.mjs"
655
+ nonce={CSPnonce}
656
+ integrity="init2hash"
657
+ />,
658
<div>Hello</div>,
659
]);
660
} finally {
@@ -3783,6 +3800,9 @@ describe('ReactDOMFizzServer', () => {
3800
<link rel="preload" href="foo" as="script" />
3801
<link rel="preload" href="bar" as="script" />
3802
<link rel="preload" href="baz" as="script" integrity="qux" />
3803
+ <link rel="modulepreload" href="quux" />
3804
+ <link rel="modulepreload" href="corge" />
3805
+ <link rel="modulepreload" href="grault" integrity="garply" />
3806
</head>
3807
<body>
3808
<div>hello world</div>
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+2
-2
@@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => {
84
);
85
const result = await readResult(stream);
86
expect(result).toMatchInlineSnapshot(
87
- `"<link rel="preload" href="init.js" as="script"/><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" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
88
);
89
});
90
@@ -500,7 +500,7 @@ describe('ReactDOMFizzServerBrowser', () => {
500
);
501
const result = await readResult(stream);
502
expect(result).toMatchInlineSnapshot(
503
- `"<link rel="preload" href="init.js" as="script" 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>"`,
503
+ `"<link rel="preload" href="init.js" as="script" nonce="R4nd0m"/><link rel="modulepreload" href="init.mjs" 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>"`,
504
);
505
});
506
});
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+1
-1
@@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => {
98
pipe(writable);
99
jest.runAllTimers();
100
expect(output.result).toMatchInlineSnapshot(
101
- `"<link rel="preload" href="init.js" as="script"/><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" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
102
);
103
});
104
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+1
-1
@@ -84,7 +84,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
84
});
85
const prelude = await readContent(result.prelude);
86
expect(prelude).toMatchInlineSnapshot(
87
- `"<link rel="preload" href="init.js" as="script"/><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" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
88
);
89
});
90
packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
+1
-1
@@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => {
86
);
87
const prelude = await readContent(result.prelude);
88
expect(prelude).toMatchInlineSnapshot(
89
- `"<link rel="preload" href="init.js" as="script"/><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" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
90
);
91
});
92
packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js
+1
-1
@@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => {
59
});
60
const result = readResult(stream);
61
expect(result).toMatchInlineSnapshot(
62
- `"<link rel="preload" href="init.js" as="script"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
62
+ `"<link rel="preload" href="init.js" as="script"/><link rel="modulepreload" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
63
);
64
});
65