Skip to content

Commit

Permalink
[Fizz] preload bootstrapScripts (#26753)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnoff committed May 31, 2023
1 parent e1e68b9 commit b864ad4
Show file tree
Hide file tree
Showing 19 changed files with 135 additions and 16 deletions.
43 changes: 43 additions & 0 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export type ExternalRuntimeScript = {
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
// is set, the server will send instructions via data attributes (instead of inline scripts)
export function createResponseState(
resources: Resources,
identifierPrefix: string | void,
nonce: string | void,
bootstrapScriptContent: string | void,
Expand Down Expand Up @@ -266,6 +267,8 @@ export function createResponseState(
const integrity =
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;

preloadBootstrapScript(resources, src, nonce, integrity);

bootstrapChunks.push(
startScriptSrc,
stringToChunk(escapeTextForBrowser(src)),
Expand Down Expand Up @@ -5469,6 +5472,46 @@ function preinit(href: string, options: PreinitOptions): void {
}
}

// This function is only safe to call at Request start time since it assumes
// that each script has not already been preloaded. If we find a need to preload
// scripts at any other point in time we will need to check whether the preload
// already exists and not assume it
function preloadBootstrapScript(
resources: Resources,
src: string,
nonce: ?string,
integrity: ?string,
): void {
const key = getResourceKey('script', src);
if (__DEV__) {
if (resources.preloadsMap.has(key)) {
// This is coded as a React error because it should be impossible for a userspace preload to preempt this call
// If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
// rather than instruct the user to not preload their bootstrap scripts themselves
console.error(
'Internal React Error: React expected bootstrap script with src "%s" to not have been preloaded already. please file an issue',
src,
);
}
}
const props: PreloadProps = {
rel: 'preload',
href: src,
as: 'script',
nonce,
integrity,
};
const resource: PreloadResource = {
type: 'preload',
chunks: [],
state: NoState,
props,
};
resources.preloadsMap.set(key, resource);
resources.explicitScriptPreloads.add(resource);
pushLinkImpl(resource.chunks, props);
}

function internalPreinitScript(
resources: Resources,
src: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type {
Resources,
BootstrapScriptDescriptor,
ExternalRuntimeScript,
FormatContext,
Expand Down Expand Up @@ -63,11 +64,13 @@ export type ResponseState = {
};

export function createResponseState(
resources: Resources,
generateStaticMarkup: boolean,
identifierPrefix: string | void,
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
): ResponseState {
const responseState = createResponseStateImpl(
resources,
identifierPrefix,
undefined,
undefined,
Expand Down
37 changes: 32 additions & 5 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,27 +596,50 @@ describe('ReactDOMFizzServer', () => {
{
nonce: 'R4nd0m',
bootstrapScriptContent: 'function noop(){}',
bootstrapScripts: ['init.js'],
bootstrapScripts: [
'init.js',
{src: 'init2.js', integrity: 'init2hash'},
],
bootstrapModules: ['init.mjs'],
},
);
pipe(writable);
});

expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
expect(getVisibleChildren(container)).toEqual([
<link rel="preload" href="init.js" as="script" nonce={CSPnonce} />,
<link
rel="preload"
href="init2.js"
as="script"
nonce={CSPnonce}
integrity="init2hash"
/>,
<div>Loading...</div>,
]);

// check that there are 4 scripts with a matching nonce:
// The runtime script, an inline bootstrap script, and two src scripts
expect(
Array.from(container.getElementsByTagName('script')).filter(
node => node.getAttribute('nonce') === CSPnonce,
).length,
).toEqual(4);
).toEqual(5);

await act(() => {
resolve({default: Text});
});
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
expect(getVisibleChildren(container)).toEqual([
<link rel="preload" href="init.js" as="script" nonce={CSPnonce} />,
<link
rel="preload"
href="init2.js"
as="script"
nonce={CSPnonce}
integrity="init2hash"
/>,
<div>Hello</div>,
]);
} finally {
CSPnonce = null;
}
Expand Down Expand Up @@ -3756,7 +3779,11 @@ describe('ReactDOMFizzServer', () => {

expect(getVisibleChildren(document)).toEqual(
<html>
<head />
<head>
<link rel="preload" href="foo" as="script" />
<link rel="preload" href="bar" as="script" />
<link rel="preload" href="baz" as="script" integrity="qux" />
</head>
<body>
<div>hello world</div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<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>"`,
);
});

Expand Down Expand Up @@ -500,7 +500,7 @@ describe('ReactDOMFizzServerBrowser', () => {
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<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>"`,
`"<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>"`,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => {
pipe(writable);
jest.runAllTimers();
expect(output.result).toMatchInlineSnapshot(
`"<div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<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>"`,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
});
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<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>"`,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => {
);
const prelude = await readContent(result.prelude);
expect(prelude).toMatchInlineSnapshot(
`"<div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
`"<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>"`,
);
});

Expand Down
4 changes: 4 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
Expand Down Expand Up @@ -79,9 +80,12 @@ function renderToReadableStream(
allReady.catch(() => {});
reject(error);
}
const resources = createResources();
const request = createRequest(
children,
resources,
createResponseState(
resources,
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
Expand Down Expand Up @@ -80,9 +81,12 @@ function renderToReadableStream(
allReady.catch(() => {});
reject(error);
}
const resources = createResources();
const request = createRequest(
children,
resources,
createResponseState(
resources,
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
Expand Down Expand Up @@ -79,9 +80,12 @@ function renderToReadableStream(
allReady.catch(() => {});
reject(error);
}
const resources = createResources();
const request = createRequest(
children,
resources,
createResponseState(
resources,
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
Expand Down Expand Up @@ -59,9 +60,12 @@ type PipeableStream = {
};

function createRequestImpl(children: ReactNodeList, options: void | Options) {
const resources = createResources();
return createRequest(
children,
resources,
createResponseState(
resources,
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
Expand Down Expand Up @@ -64,9 +65,12 @@ function prerender(
};
resolve(result);
}
const resources = createResources();
const request = createRequest(
children,
resources,
createResponseState(
resources,
options ? options.identifierPrefix : undefined,
undefined,
options ? options.bootstrapScriptContent : undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
Expand Down Expand Up @@ -64,9 +65,12 @@ function prerender(
};
resolve(result);
}
const resources = createResources();
const request = createRequest(
children,
resources,
createResponseState(
resources,
options ? options.identifierPrefix : undefined,
undefined,
options ? options.bootstrapScriptContent : undefined,
Expand Down
5 changes: 4 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzStaticNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
Expand Down Expand Up @@ -78,10 +79,12 @@ function prerenderToNodeStreams(
};
resolve(result);
}

const resources = createResources();
const request = createRequest(
children,
resources,
createResponseState(
resources,
options ? options.identifierPrefix : undefined,
undefined,
options ? options.bootstrapScriptContent : undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-dom/src/server/ReactDOMLegacyServerImpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
Expand Down Expand Up @@ -61,9 +62,12 @@ function renderToStringImpl(
function onShellReady() {
readyToStream = true;
}
const resources = createResources();
const request = createRequest(
children,
resources,
createResponseState(
resources,
generateStaticMarkup,
options ? options.identifierPrefix : undefined,
unstable_externalRuntimeSrc,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from 'react-server/src/ReactFizzServer';

import {
createResources,
createResponseState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
Expand Down Expand Up @@ -70,9 +71,15 @@ function renderToNodeStreamImpl(
startFlowing(request, destination);
}
const destination = new ReactMarkupReadableStream();
const resources = createResources();
const request = createRequest(
children,
createResponseState(false, options ? options.identifierPrefix : undefined),
resources,
createResponseState(
resources,
false,
options ? options.identifierPrefix : undefined,
),
createRootFormatContext(),
Infinity,
onError,
Expand Down
Loading

0 comments on commit b864ad4

Please sign in to comment.