@samitouri / QOS-React / commits / ae31d2ea3c

[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