@samitouri / QOS-React-2 / commits / 8ea96ef84d

[Fizz] Encode external fizz runtime into chunks eagerly (#26752)

in https://github.com/facebook/react/pull/26738 we added nonce to the ResponseState. Initially it was used in a variety of places but the version that got merged only included it with the external fizz runtime. This PR updates the config for the external fizz runtime so that the nonce is encoded into the script chunks at request creation time. The rationale is that for live-requests, streaming is more likely than not so doing the encoding work at the start is better than during flush. For cases such as SSG where the runtime is not required the extra encoding is tolerable (not a live request). Bots are an interesting case because if you want fastest TTFB you will end up requiring the runtime but if you are withholding until the stream is done you have already sacrificed fastest TTFB and the marginal slowdown of the extraneous encoding is hopefully neglibible I'm writing this so later if we learn that this tradeoff isn't worth it we at least understand why I made the change in the first place.

Josh Story committed May 1, 2023 at 10:50 UTC 8ea96ef84d8f08ed1846dec9e8ed20d2225db0d3
3 files changed +35 -30
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+31 -25
@@ -130,11 +130,8 @@ export type ResponseState = {
130 startInlineScript: PrecomputedChunk,
131 instructions: InstructionState,
132
133 - // state for outputting CSP nonce
134 - nonce: string | void,
135 -
133 // state for data streaming format
137 - externalRuntimeConfig: BootstrapScriptDescriptor | null,
134 + externalRuntimeScript: null | ExternalRuntimeScript,
135
136 // preamble and postamble chunks and state
137 htmlChunks: null | Array<Chunk | PrecomputedChunk>,
@@ -196,6 +193,10 @@ export type BootstrapScriptDescriptor = {
193 src: string,
194 integrity?: string,
195 };
196 +export type ExternalRuntimeScript = {
197 + src: string,
198 + chunks: Array<Chunk | PrecomputedChunk>,
199 +};
200 // Allows us to keep track of what we've already written so we can refer back to it.
201 // if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
202 // is set, the server will send instructions via data attributes (instead of inline scripts)
@@ -215,7 +216,7 @@ export function createResponseState(
216 '<script nonce="' + escapeTextForBrowser(nonce) + '">',
217 );
218 const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];
218 - let externalRuntimeDesc = null;
219 + let externalRuntimeScript: null | ExternalRuntimeScript = null;
220 let streamingFormat = ScriptStreamingFormat;
221 if (bootstrapScriptContent !== undefined) {
222 bootstrapChunks.push(
@@ -233,12 +234,27 @@ export function createResponseState(
234 if (externalRuntimeConfig !== undefined) {
235 streamingFormat = DataStreamingFormat;
236 if (typeof externalRuntimeConfig === 'string') {
236 - externalRuntimeDesc = {
237 + externalRuntimeScript = {
238 src: externalRuntimeConfig,
238 - integrity: undefined,
239 + chunks: [],
240 };
241 + pushScriptImpl(externalRuntimeScript.chunks, {
242 + src: externalRuntimeConfig,
243 + async: true,
244 + integrity: undefined,
245 + nonce: nonce,
246 + });
247 } else {
241 - externalRuntimeDesc = externalRuntimeConfig;
248 + externalRuntimeScript = {
249 + src: externalRuntimeConfig.src,
250 + chunks: [],
251 + };
252 + pushScriptImpl(externalRuntimeScript.chunks, {
253 + src: externalRuntimeConfig.src,
254 + async: true,
255 + integrity: externalRuntimeConfig.integrity,
256 + nonce: nonce,
257 + });
258 }
259 }
260 }
@@ -307,7 +323,7 @@ export function createResponseState(
323 streamingFormat,
324 startInlineScript: inlineScriptWithNonce,
325 instructions: NothingSent,
310 - externalRuntimeConfig: externalRuntimeDesc,
326 + externalRuntimeScript,
327 htmlChunks: null,
328 headChunks: null,
329 hasBody: false,
@@ -1293,7 +1309,7 @@ function injectFormReplayingRuntime(responseState: ResponseState): void {
1309 // to emit anything. It's always used.
1310 if (
1311 (responseState.instructions & SentFormReplayingRuntime) === NothingSent &&
1296 - (!enableFizzExternalRuntime || !responseState.externalRuntimeConfig)
1312 + (!enableFizzExternalRuntime || !responseState.externalRuntimeScript)
1313 ) {
1314 responseState.instructions |= SentFormReplayingRuntime;
1315 responseState.bootstrapChunks.unshift(
@@ -4078,15 +4094,15 @@ export function writePreamble(
4094 if (
4095 enableFizzExternalRuntime &&
4096 !willFlushAllSegments &&
4081 - responseState.externalRuntimeConfig
4097 + responseState.externalRuntimeScript
4098 ) {
4099 // If the root segment is incomplete due to suspended tasks
4100 // (e.g. willFlushAllSegments = false) and we are using data
4101 // streaming format, ensure the external runtime is sent.
4102 // (User code could choose to send this even earlier by calling
4103 // preinit(...), if they know they will suspend).
4088 - const {src, integrity} = responseState.externalRuntimeConfig;
4089 - internalPreinitScript(resources, src, integrity, responseState.nonce);
4104 + const {src, chunks} = responseState.externalRuntimeScript;
4105 + internalPreinitScript(resources, src, chunks);
4106 }
4107
4108 const htmlChunks = responseState.htmlChunks;
@@ -5362,32 +5378,22 @@ function preinit(href: string, options: PreinitOptions): void {
5378 }
5379 }
5380
5365 -// This method is trusted. It must only be called from within this codebase and it assumes the arguments
5366 -// conform to the types because no user input is being passed in. It also assumes that it is being called as
5367 -// part of a work or flush loop and therefore does not need to request Fizz to flush Resources.
5381 function internalPreinitScript(
5382 resources: Resources,
5383 src: string,
5371 - integrity: ?string,
5372 - nonce: ?string,
5384 + chunks: Array<Chunk | PrecomputedChunk>,
5385 ): void {
5386 const key = getResourceKey('script', src);
5387 let resource = resources.scriptsMap.get(key);
5388 if (!resource) {
5389 resource = {
5390 type: 'script',
5379 - chunks: [],
5391 + chunks,
5392 state: NoState,
5393 props: null,
5394 };
5395 resources.scriptsMap.set(key, resource);
5396 resources.scripts.add(resource);
5385 - pushScriptImpl(resource.chunks, {
5386 - async: true,
5387 - src,
5388 - integrity,
5389 - nonce,
5390 - });
5397 }
5398 return;
5399 }
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+3 -4
@@ -9,6 +9,7 @@
9
10 import type {
11 BootstrapScriptDescriptor,
12 + ExternalRuntimeScript,
13 FormatContext,
14 StreamingFormat,
15 InstructionState,
@@ -48,7 +49,7 @@ export type ResponseState = {
49 streamingFormat: StreamingFormat,
50 startInlineScript: PrecomputedChunk,
51 instructions: InstructionState,
51 - externalRuntimeConfig: BootstrapScriptDescriptor | null,
52 + externalRuntimeScript: null | ExternalRuntimeScript,
53 htmlChunks: null | Array<Chunk | PrecomputedChunk>,
54 headChunks: null | Array<Chunk | PrecomputedChunk>,
55 hasBody: boolean,
@@ -57,7 +58,6 @@ export type ResponseState = {
58 preloadChunks: Array<Chunk | PrecomputedChunk>,
59 hoistableChunks: Array<Chunk | PrecomputedChunk>,
60 stylesToHoist: boolean,
60 - nonce: string | void,
61 // This is an extra field for the legacy renderer
62 generateStaticMarkup: boolean,
63 };
@@ -86,7 +86,7 @@ export function createResponseState(
86 streamingFormat: responseState.streamingFormat,
87 startInlineScript: responseState.startInlineScript,
88 instructions: responseState.instructions,
89 - externalRuntimeConfig: responseState.externalRuntimeConfig,
89 + externalRuntimeScript: responseState.externalRuntimeScript,
90 htmlChunks: responseState.htmlChunks,
91 headChunks: responseState.headChunks,
92 hasBody: responseState.hasBody,
@@ -95,7 +95,6 @@ export function createResponseState(
95 preloadChunks: responseState.preloadChunks,
96 hoistableChunks: responseState.hoistableChunks,
97 stylesToHoist: responseState.stylesToHoist,
98 - nonce: responseState.nonce,
98
99 // This is an extra field for the legacy renderer
100 generateStaticMarkup,
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+1 -1
@@ -3767,7 +3767,7 @@ describe('ReactDOMFizzServer', () => {
3767 Array.from(document.head.getElementsByTagName('script')).map(
3768 n => n.outerHTML,
3769 ),
3770 - ).toEqual(['<script async="" src="src-of-external-runtime"></script>']);
3770 + ).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
3771
3772 expect(getVisibleChildren(document)).toEqual(
3773 <html>