[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