Skip to content

Commit b864ad4

Browse files
authored
[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.
1 parent e1e68b9 commit b864ad4

19 files changed

+135
-16
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export type ExternalRuntimeScript = {
201201
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
202202
// is set, the server will send instructions via data attributes (instead of inline scripts)
203203
export function createResponseState(
204+
resources: Resources,
204205
identifierPrefix: string | void,
205206
nonce: string | void,
206207
bootstrapScriptContent: string | void,
@@ -266,6 +267,8 @@ export function createResponseState(
266267
const integrity =
267268
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
268269

270+
preloadBootstrapScript(resources, src, nonce, integrity);
271+
269272
bootstrapChunks.push(
270273
startScriptSrc,
271274
stringToChunk(escapeTextForBrowser(src)),
@@ -5469,6 +5472,46 @@ function preinit(href: string, options: PreinitOptions): void {
54695472
}
54705473
}
54715474

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+
54725515
function internalPreinitScript(
54735516
resources: Resources,
54745517
src: string,

packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type {
11+
Resources,
1112
BootstrapScriptDescriptor,
1213
ExternalRuntimeScript,
1314
FormatContext,
@@ -63,11 +64,13 @@ export type ResponseState = {
6364
};
6465

6566
export function createResponseState(
67+
resources: Resources,
6668
generateStaticMarkup: boolean,
6769
identifierPrefix: string | void,
6870
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
6971
): ResponseState {
7072
const responseState = createResponseStateImpl(
73+
resources,
7174
identifierPrefix,
7275
undefined,
7376
undefined,

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -596,27 +596,50 @@ describe('ReactDOMFizzServer', () => {
596596
{
597597
nonce: 'R4nd0m',
598598
bootstrapScriptContent: 'function noop(){}',
599-
bootstrapScripts: ['init.js'],
599+
bootstrapScripts: [
600+
'init.js',
601+
{src: 'init2.js', integrity: 'init2hash'},
602+
],
600603
bootstrapModules: ['init.mjs'],
601604
},
602605
);
603606
pipe(writable);
604607
});
605608

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+
]);
607620

608621
// check that there are 4 scripts with a matching nonce:
609622
// The runtime script, an inline bootstrap script, and two src scripts
610623
expect(
611624
Array.from(container.getElementsByTagName('script')).filter(
612625
node => node.getAttribute('nonce') === CSPnonce,
613626
).length,
614-
).toEqual(4);
627+
).toEqual(5);
615628

616629
await act(() => {
617630
resolve({default: Text});
618631
});
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+
]);
620643
} finally {
621644
CSPnonce = null;
622645
}
@@ -3756,7 +3779,11 @@ describe('ReactDOMFizzServer', () => {
37563779

37573780
expect(getVisibleChildren(document)).toEqual(
37583781
<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>
37603787
<body>
37613788
<div>hello world</div>
37623789
</body>

packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => {
8484
);
8585
const result = await readResult(stream);
8686
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>"`,
8888
);
8989
});
9090

@@ -500,7 +500,7 @@ describe('ReactDOMFizzServerBrowser', () => {
500500
);
501501
const result = await readResult(stream);
502502
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>"`,
504504
);
505505
});
506506
});

packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => {
9898
pipe(writable);
9999
jest.runAllTimers();
100100
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>"`,
102102
);
103103
});
104104

packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
8484
});
8585
const prelude = await readContent(result.prelude);
8686
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>"`,
8888
);
8989
});
9090

packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => {
8686
);
8787
const prelude = await readContent(result.prelude);
8888
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>"`,
9090
);
9191
});
9292

packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'react-server/src/ReactFizzServer';
2121

2222
import {
23+
createResources,
2324
createResponseState,
2425
createRootFormatContext,
2526
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -79,9 +80,12 @@ function renderToReadableStream(
7980
allReady.catch(() => {});
8081
reject(error);
8182
}
83+
const resources = createResources();
8284
const request = createRequest(
8385
children,
86+
resources,
8487
createResponseState(
88+
resources,
8589
options ? options.identifierPrefix : undefined,
8690
options ? options.nonce : undefined,
8791
options ? options.bootstrapScriptContent : undefined,

packages/react-dom/src/server/ReactDOMFizzServerBun.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'react-server/src/ReactFizzServer';
2121

2222
import {
23+
createResources,
2324
createResponseState,
2425
createRootFormatContext,
2526
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -80,9 +81,12 @@ function renderToReadableStream(
8081
allReady.catch(() => {});
8182
reject(error);
8283
}
84+
const resources = createResources();
8385
const request = createRequest(
8486
children,
87+
resources,
8588
createResponseState(
89+
resources,
8690
options ? options.identifierPrefix : undefined,
8791
options ? options.nonce : undefined,
8892
options ? options.bootstrapScriptContent : undefined,

packages/react-dom/src/server/ReactDOMFizzServerEdge.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'react-server/src/ReactFizzServer';
2121

2222
import {
23+
createResources,
2324
createResponseState,
2425
createRootFormatContext,
2526
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -79,9 +80,12 @@ function renderToReadableStream(
7980
allReady.catch(() => {});
8081
reject(error);
8182
}
83+
const resources = createResources();
8284
const request = createRequest(
8385
children,
86+
resources,
8487
createResponseState(
88+
resources,
8589
options ? options.identifierPrefix : undefined,
8690
options ? options.nonce : undefined,
8791
options ? options.bootstrapScriptContent : undefined,

packages/react-dom/src/server/ReactDOMFizzServerNode.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from 'react-server/src/ReactFizzServer';
2424

2525
import {
26+
createResources,
2627
createResponseState,
2728
createRootFormatContext,
2829
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -59,9 +60,12 @@ type PipeableStream = {
5960
};
6061

6162
function createRequestImpl(children: ReactNodeList, options: void | Options) {
63+
const resources = createResources();
6264
return createRequest(
6365
children,
66+
resources,
6467
createResponseState(
68+
resources,
6569
options ? options.identifierPrefix : undefined,
6670
options ? options.nonce : undefined,
6771
options ? options.bootstrapScriptContent : undefined,

packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'react-server/src/ReactFizzServer';
2121

2222
import {
23+
createResources,
2324
createResponseState,
2425
createRootFormatContext,
2526
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -64,9 +65,12 @@ function prerender(
6465
};
6566
resolve(result);
6667
}
68+
const resources = createResources();
6769
const request = createRequest(
6870
children,
71+
resources,
6972
createResponseState(
73+
resources,
7074
options ? options.identifierPrefix : undefined,
7175
undefined,
7276
options ? options.bootstrapScriptContent : undefined,

packages/react-dom/src/server/ReactDOMFizzStaticEdge.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'react-server/src/ReactFizzServer';
2121

2222
import {
23+
createResources,
2324
createResponseState,
2425
createRootFormatContext,
2526
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -64,9 +65,12 @@ function prerender(
6465
};
6566
resolve(result);
6667
}
68+
const resources = createResources();
6769
const request = createRequest(
6870
children,
71+
resources,
6972
createResponseState(
73+
resources,
7074
options ? options.identifierPrefix : undefined,
7175
undefined,
7276
options ? options.bootstrapScriptContent : undefined,

packages/react-dom/src/server/ReactDOMFizzStaticNode.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from 'react-server/src/ReactFizzServer';
2323

2424
import {
25+
createResources,
2526
createResponseState,
2627
createRootFormatContext,
2728
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
@@ -78,10 +79,12 @@ function prerenderToNodeStreams(
7879
};
7980
resolve(result);
8081
}
81-
82+
const resources = createResources();
8283
const request = createRequest(
8384
children,
85+
resources,
8486
createResponseState(
87+
resources,
8588
options ? options.identifierPrefix : undefined,
8689
undefined,
8790
options ? options.bootstrapScriptContent : undefined,

packages/react-dom/src/server/ReactDOMLegacyServerImpl.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'react-server/src/ReactFizzServer';
2121

2222
import {
23+
createResources,
2324
createResponseState,
2425
createRootFormatContext,
2526
} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
@@ -61,9 +62,12 @@ function renderToStringImpl(
6162
function onShellReady() {
6263
readyToStream = true;
6364
}
65+
const resources = createResources();
6466
const request = createRequest(
6567
children,
68+
resources,
6669
createResponseState(
70+
resources,
6771
generateStaticMarkup,
6872
options ? options.identifierPrefix : undefined,
6973
unstable_externalRuntimeSrc,

packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from 'react-server/src/ReactFizzServer';
2020

2121
import {
22+
createResources,
2223
createResponseState,
2324
createRootFormatContext,
2425
} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
@@ -70,9 +71,15 @@ function renderToNodeStreamImpl(
7071
startFlowing(request, destination);
7172
}
7273
const destination = new ReactMarkupReadableStream();
74+
const resources = createResources();
7375
const request = createRequest(
7476
children,
75-
createResponseState(false, options ? options.identifierPrefix : undefined),
77+
resources,
78+
createResponseState(
79+
resources,
80+
false,
81+
options ? options.identifierPrefix : undefined,
82+
),
7683
createRootFormatContext(),
7784
Infinity,
7885
onError,

0 commit comments

Comments
 (0)