@samitouri / QOS-React / commits / b864ad4397

[Fizz] preload `bootstrapScripts` (#26753)

This PR adds a preload for bootstrapScripts. preloads are captured synchronously when you create a new Request and as such the normal logic to check if a preload already exists is skipped.

Josh Story committed May 31, 2023 at 16:25 UTC b864ad4397e7b366ece9ecfdfafa5660ae6b8390
19 files changed +135 -16
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+43
@@ -201,6 +201,7 @@ export type ExternalRuntimeScript = {
201 // if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
202 // is set, the server will send instructions via data attributes (instead of inline scripts)
203 export function createResponseState(
204 + resources: Resources,
205 identifierPrefix: string | void,
206 nonce: string | void,
207 bootstrapScriptContent: string | void,
@@ -266,6 +267,8 @@ export function createResponseState(
267 const integrity =
268 typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
269
270 + preloadBootstrapScript(resources, src, nonce, integrity);
271 +
272 bootstrapChunks.push(
273 startScriptSrc,
274 stringToChunk(escapeTextForBrowser(src)),
@@ -5469,6 +5472,46 @@ function preinit(href: string, options: PreinitOptions): void {
5472 }
5473 }
5474
5475 +// This function is only safe to call at Request start time since it assumes
5476 +// that each script has not already been preloaded. If we find a need to preload
5477 +// scripts at any other point in time we will need to check whether the preload
5478 +// already exists and not assume it
5479 +function preloadBootstrapScript(
5480 + resources: Resources,
5481 + src: string,
5482 + nonce: ?string,
5483 + integrity: ?string,
5484 +): void {
5485 + const key = getResourceKey('script', src);
5486 + if (__DEV__) {
5487 + if (resources.preloadsMap.has(key)) {
5488 + // This is coded as a React error because it should be impossible for a userspace preload to preempt this call
5489 + // If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
5490 + // rather than instruct the user to not preload their bootstrap scripts themselves
5491 + console.error(
5492 + 'Internal React Error: React expected bootstrap script with src "%s" to not have been preloaded already. please file an issue',
5493 + src,
5494 + );
5495 + }
5496 + }
5497 + const props: PreloadProps = {
5498 + rel: 'preload',
5499 + href: src,
5500 + as: 'script',
5501 + nonce,
5502 + integrity,
5503 + };
5504 + const resource: PreloadResource = {
5505 + type: 'preload',
5506 + chunks: [],
5507 + state: NoState,
5508 + props,
5509 + };
5510 + resources.preloadsMap.set(key, resource);
5511 + resources.explicitScriptPreloads.add(resource);
5512 + pushLinkImpl(resource.chunks, props);
5513 +}
5514 +
5515 function internalPreinitScript(
5516 resources: Resources,
5517 src: string,
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+3
@@ -8,6 +8,7 @@
8 */
9
10 import type {
11 + Resources,
12 BootstrapScriptDescriptor,
13 ExternalRuntimeScript,
14 FormatContext,
@@ -63,11 +64,13 @@ export type ResponseState = {
64 };
65
66 export function createResponseState(
67 + resources: Resources,
68 generateStaticMarkup: boolean,
69 identifierPrefix: string | void,
70 externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
71 ): ResponseState {
72 const responseState = createResponseStateImpl(
73 + resources,
74 identifierPrefix,
75 undefined,
76 undefined,
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+32 -5
@@ -596,14 +596,27 @@ describe('ReactDOMFizzServer', () => {
596 {
597 nonce: 'R4nd0m',
598 bootstrapScriptContent: 'function noop(){}',
599 - bootstrapScripts: ['init.js'],
599 + bootstrapScripts: [
600 + 'init.js',
601 + {src: 'init2.js', integrity: 'init2hash'},
602 + ],
603 bootstrapModules: ['init.mjs'],
604 },
605 );
606 pipe(writable);
607 });
608
606 - expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
609 + expect(getVisibleChildren(container)).toEqual([
610 + <link rel="preload" href="init.js" as="script" nonce={CSPnonce} />,
611 + <link
612 + rel="preload"
613 + href="init2.js"
614 + as="script"
615 + nonce={CSPnonce}
616 + integrity="init2hash"
617 + />,
618 + <div>Loading...</div>,
619 + ]);
620
621 // check that there are 4 scripts with a matching nonce:
622 // The runtime script, an inline bootstrap script, and two src scripts
@@ -611,12 +624,22 @@ describe('ReactDOMFizzServer', () => {
624 Array.from(container.getElementsByTagName('script')).filter(
625 node => node.getAttribute('nonce') === CSPnonce,
626 ).length,
614 - ).toEqual(4);
627 + ).toEqual(5);
628
629 await act(() => {
630 resolve({default: Text});
631 });
619 - expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
632 + expect(getVisibleChildren(container)).toEqual([
633 + <link rel="preload" href="init.js" as="script" nonce={CSPnonce} />,
634 + <link
635 + rel="preload"
636 + href="init2.js"
637 + as="script"
638 + nonce={CSPnonce}
639 + integrity="init2hash"
640 + />,
641 + <div>Hello</div>,
642 + ]);
643 } finally {
644 CSPnonce = null;
645 }
@@ -3756,7 +3779,11 @@ describe('ReactDOMFizzServer', () => {
3779
3780 expect(getVisibleChildren(document)).toEqual(
3781 <html>
3759 - <head />
3782 + <head>
3783 + <link rel="preload" href="foo" as="script" />
3784 + <link rel="preload" href="bar" as="script" />
3785 + <link rel="preload" href="baz" as="script" integrity="qux" />
3786 + </head>
3787 <body>
3788 <div>hello world</div>
3789 </body>
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 - `"<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"/><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 - `"<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"/><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 - `"<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"/><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 - `"<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"/><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 - `"<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"/><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-dom/src/server/ReactDOMFizzServerBrowser.js
+4
@@ -20,6 +20,7 @@ import {
20 } from 'react-server/src/ReactFizzServer';
21
22 import {
23 + createResources,
24 createResponseState,
25 createRootFormatContext,
26 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -79,9 +80,12 @@ function renderToReadableStream(
80 allReady.catch(() => {});
81 reject(error);
82 }
83 + const resources = createResources();
84 const request = createRequest(
85 children,
86 + resources,
87 createResponseState(
88 + resources,
89 options ? options.identifierPrefix : undefined,
90 options ? options.nonce : undefined,
91 options ? options.bootstrapScriptContent : undefined,
packages/react-dom/src/server/ReactDOMFizzServerBun.js
+4
@@ -20,6 +20,7 @@ import {
20 } from 'react-server/src/ReactFizzServer';
21
22 import {
23 + createResources,
24 createResponseState,
25 createRootFormatContext,
26 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -80,9 +81,12 @@ function renderToReadableStream(
81 allReady.catch(() => {});
82 reject(error);
83 }
84 + const resources = createResources();
85 const request = createRequest(
86 children,
87 + resources,
88 createResponseState(
89 + resources,
90 options ? options.identifierPrefix : undefined,
91 options ? options.nonce : undefined,
92 options ? options.bootstrapScriptContent : undefined,
packages/react-dom/src/server/ReactDOMFizzServerEdge.js
+4
@@ -20,6 +20,7 @@ import {
20 } from 'react-server/src/ReactFizzServer';
21
22 import {
23 + createResources,
24 createResponseState,
25 createRootFormatContext,
26 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -79,9 +80,12 @@ function renderToReadableStream(
80 allReady.catch(() => {});
81 reject(error);
82 }
83 + const resources = createResources();
84 const request = createRequest(
85 children,
86 + resources,
87 createResponseState(
88 + resources,
89 options ? options.identifierPrefix : undefined,
90 options ? options.nonce : undefined,
91 options ? options.bootstrapScriptContent : undefined,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+4
@@ -23,6 +23,7 @@ import {
23 } from 'react-server/src/ReactFizzServer';
24
25 import {
26 + createResources,
27 createResponseState,
28 createRootFormatContext,
29 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -59,9 +60,12 @@ type PipeableStream = {
60 };
61
62 function createRequestImpl(children: ReactNodeList, options: void | Options) {
63 + const resources = createResources();
64 return createRequest(
65 children,
66 + resources,
67 createResponseState(
68 + resources,
69 options ? options.identifierPrefix : undefined,
70 options ? options.nonce : undefined,
71 options ? options.bootstrapScriptContent : undefined,
packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+4
@@ -20,6 +20,7 @@ import {
20 } from 'react-server/src/ReactFizzServer';
21
22 import {
23 + createResources,
24 createResponseState,
25 createRootFormatContext,
26 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -64,9 +65,12 @@ function prerender(
65 };
66 resolve(result);
67 }
68 + const resources = createResources();
69 const request = createRequest(
70 children,
71 + resources,
72 createResponseState(
73 + resources,
74 options ? options.identifierPrefix : undefined,
75 undefined,
76 options ? options.bootstrapScriptContent : undefined,
packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
+4
@@ -20,6 +20,7 @@ import {
20 } from 'react-server/src/ReactFizzServer';
21
22 import {
23 + createResources,
24 createResponseState,
25 createRootFormatContext,
26 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -64,9 +65,12 @@ function prerender(
65 };
66 resolve(result);
67 }
68 + const resources = createResources();
69 const request = createRequest(
70 children,
71 + resources,
72 createResponseState(
73 + resources,
74 options ? options.identifierPrefix : undefined,
75 undefined,
76 options ? options.bootstrapScriptContent : undefined,
packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+4 -1
@@ -22,6 +22,7 @@ import {
22 } from 'react-server/src/ReactFizzServer';
23
24 import {
25 + createResources,
26 createResponseState,
27 createRootFormatContext,
28 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -78,10 +79,12 @@ function prerenderToNodeStreams(
79 };
80 resolve(result);
81 }
81 -
82 + const resources = createResources();
83 const request = createRequest(
84 children,
85 + resources,
86 createResponseState(
87 + resources,
88 options ? options.identifierPrefix : undefined,
89 undefined,
90 options ? options.bootstrapScriptContent : undefined,
packages/react-dom/src/server/ReactDOMLegacyServerImpl.js
+4
@@ -20,6 +20,7 @@ import {
20 } from 'react-server/src/ReactFizzServer';
21
22 import {
23 + createResources,
24 createResponseState,
25 createRootFormatContext,
26 } from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
@@ -61,9 +62,12 @@ function renderToStringImpl(
62 function onShellReady() {
63 readyToStream = true;
64 }
65 + const resources = createResources();
66 const request = createRequest(
67 children,
68 + resources,
69 createResponseState(
70 + resources,
71 generateStaticMarkup,
72 options ? options.identifierPrefix : undefined,
73 unstable_externalRuntimeSrc,
packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js
+8 -1
@@ -19,6 +19,7 @@ import {
19 } from 'react-server/src/ReactFizzServer';
20
21 import {
22 + createResources,
23 createResponseState,
24 createRootFormatContext,
25 } from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
@@ -70,9 +71,15 @@ function renderToNodeStreamImpl(
71 startFlowing(request, destination);
72 }
73 const destination = new ReactMarkupReadableStream();
74 + const resources = createResources();
75 const request = createRequest(
76 children,
75 - createResponseState(false, options ? options.identifierPrefix : undefined),
77 + resources,
78 + createResponseState(
79 + resources,
80 + false,
81 + options ? options.identifierPrefix : undefined,
82 + ),
83 createRootFormatContext(),
84 Infinity,
85 onError,
packages/react-server-dom-fb/src/ReactDOMServerFB.js
+4
@@ -23,6 +23,7 @@ import {
23 } from 'react-server/src/ReactFizzServer';
24
25 import {
26 + createResources,
27 createResponseState,
28 createRootFormatContext,
29 } from 'react-server/src/ReactFizzConfig';
@@ -49,9 +50,12 @@ function renderToStream(children: ReactNodeList, options: Options): Stream {
50 fatal: false,
51 error: null,
52 };
53 + const resources = createResources();
54 const request = createRequest(
55 children,
56 + resources,
57 createResponseState(
58 + resources,
59 options ? options.identifierPrefix : undefined,
60 undefined,
61 options ? options.bootstrapScriptContent : undefined,
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 - `"<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"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
63 );
64 });
65
packages/react-server/src/ReactFizzServer.js
+7 -3
@@ -72,7 +72,6 @@ import {
72 writePostamble,
73 hoistResources,
74 setCurrentlyRenderingBoundaryResourcesTarget,
75 - createResources,
75 createBoundaryResources,
76 prepareHostDispatcher,
77 supportsRequestStorage,
@@ -270,6 +269,7 @@ function noop(): void {}
269
270 export function createRequest(
271 children: ReactNodeList,
272 + resources: Resources,
273 responseState: ResponseState,
274 rootFormatContext: FormatContext,
275 progressiveChunkSize: void | number,
@@ -282,7 +282,6 @@ export function createRequest(
282 prepareHostDispatcher();
283 const pingedTasks: Array<Task> = [];
284 const abortSet: Set<Task> = new Set();
285 - const resources: Resources = createResources();
285 const request: Request = {
286 destination: null,
287 flushScheduled: false,
@@ -2343,7 +2342,12 @@ function flushCompletedQueues(
2342 // We haven't flushed the root yet so we don't need to check any other branches further down
2343 return;
2344 }
2346 - } else if (enableFloat) {
2345 + } else if (request.pendingRootTasks > 0) {
2346 + // We have not yet flushed the root segment so we early return
2347 + return;
2348 + }
2349 +
2350 + if (enableFloat) {
2351 writeHoistables(destination, request.resources, request.responseState);
2352 }
2353