[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:
Josh Story
2023-05-31 16:25:35 -07:00
committed by GitHub
parent e1e68b9f7f
commit b864ad4397
19 changed files with 135 additions and 16 deletions
@@ -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
View File
@@ -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
View File
@@ -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,
+4
View File
@@ -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
View File
@@ -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);
}