mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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.
This commit is contained in:
@@ -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,
|
||||
@@ -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)),
|
||||
@@ -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,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
Resources,
|
||||
BootstrapScriptDescriptor,
|
||||
ExternalRuntimeScript,
|
||||
FormatContext,
|
||||
@@ -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,
|
||||
|
||||
+32
-5
@@ -596,14 +596,27 @@ 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
|
||||
@@ -611,12 +624,22 @@ describe('ReactDOMFizzServer', () => {
|
||||
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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>"`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||
@@ -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,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||
@@ -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,
|
||||
|
||||
+4
-1
@@ -22,6 +22,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
|
||||
@@ -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,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
|
||||
@@ -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,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from 'react-server/src/ReactFizzServer';
|
||||
|
||||
import {
|
||||
createResources,
|
||||
createResponseState,
|
||||
createRootFormatContext,
|
||||
} from 'react-server/src/ReactFizzConfig';
|
||||
@@ -49,9 +50,12 @@ function renderToStream(children: ReactNodeList, options: Options): Stream {
|
||||
fatal: false,
|
||||
error: null,
|
||||
};
|
||||
const resources = createResources();
|
||||
const request = createRequest(
|
||||
children,
|
||||
resources,
|
||||
createResponseState(
|
||||
resources,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
undefined,
|
||||
options ? options.bootstrapScriptContent : undefined,
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => {
|
||||
});
|
||||
const result = 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>"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+7
-3
@@ -72,7 +72,6 @@ import {
|
||||
writePostamble,
|
||||
hoistResources,
|
||||
setCurrentlyRenderingBoundaryResourcesTarget,
|
||||
createResources,
|
||||
createBoundaryResources,
|
||||
prepareHostDispatcher,
|
||||
supportsRequestStorage,
|
||||
@@ -270,6 +269,7 @@ function noop(): void {}
|
||||
|
||||
export function createRequest(
|
||||
children: ReactNodeList,
|
||||
resources: Resources,
|
||||
responseState: ResponseState,
|
||||
rootFormatContext: FormatContext,
|
||||
progressiveChunkSize: void | number,
|
||||
@@ -282,7 +282,6 @@ export function createRequest(
|
||||
prepareHostDispatcher();
|
||||
const pingedTasks: Array<Task> = [];
|
||||
const abortSet: Set<Task> = new Set();
|
||||
const resources: Resources = createResources();
|
||||
const request: Request = {
|
||||
destination: null,
|
||||
flushScheduled: false,
|
||||
@@ -2343,7 +2342,12 @@ function flushCompletedQueues(
|
||||
// We haven't flushed the root yet so we don't need to check any other branches further down
|
||||
return;
|
||||
}
|
||||
} else if (enableFloat) {
|
||||
} else if (request.pendingRootTasks > 0) {
|
||||
// We have not yet flushed the root segment so we early return
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableFloat) {
|
||||
writeHoistables(destination, request.resources, request.responseState);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user