@samitouri / QOS-React-2 / commits / b94603b955

[Fizz] Gate rel="expect" behind enableFizzBlockingRender (#33183)

Enabled in experimental channel. We know this is critical semantics to enforce at the HTML level since if you don't then you can't add explicit boundaries after the fact. However, this might have to go in a major release to allow for upgrading.

Sebastian Markbåge committed May 13, 2025 at 10:17 UTC b94603b95504130aec72f61e02d7b66d48f33653
19 files changed +194 -55
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+23 -15
@@ -34,6 +34,7 @@ import {Children} from 'react';
34 import {
35 enableFizzExternalRuntime,
36 enableSrcObject,
37 + enableFizzBlockingRender,
38 } from 'shared/ReactFeatureFlags';
39
40 import type {
@@ -4146,16 +4147,21 @@ export function writeCompletedRoot(
4147 // we need to track the paint time of the shell so we know how much to throttle the reveal.
4148 writeShellTimeInstruction(destination, resumableState, renderState);
4149 }
4149 - const preamble = renderState.preamble;
4150 - if (preamble.htmlChunks || preamble.headChunks) {
4151 - // If we rendered the whole document, then we emitted a rel="expect" that needs a
4152 - // matching target. Normally we use one of the bootstrap scripts for this but if
4153 - // there are none, then we need to emit a tag to complete the shell.
4154 - if ((resumableState.instructions & SentCompletedShellId) === NothingSent) {
4155 - writeChunk(destination, startChunkForTag('template'));
4156 - writeCompletedShellIdAttribute(destination, resumableState);
4157 - writeChunk(destination, endOfStartTag);
4158 - writeChunk(destination, endChunkForTag('template'));
4150 + if (enableFizzBlockingRender) {
4151 + const preamble = renderState.preamble;
4152 + if (preamble.htmlChunks || preamble.headChunks) {
4153 + // If we rendered the whole document, then we emitted a rel="expect" that needs a
4154 + // matching target. Normally we use one of the bootstrap scripts for this but if
4155 + // there are none, then we need to emit a tag to complete the shell.
4156 + if (
4157 + (resumableState.instructions & SentCompletedShellId) ===
4158 + NothingSent
4159 + ) {
4160 + writeChunk(destination, startChunkForTag('template'));
4161 + writeCompletedShellIdAttribute(destination, resumableState);
4162 + writeChunk(destination, endOfStartTag);
4163 + writeChunk(destination, endChunkForTag('template'));
4164 + }
4165 }
4166 }
4167 return writeBootstrap(destination, renderState);
@@ -5040,11 +5046,13 @@ function writeBlockingRenderInstruction(
5046 resumableState: ResumableState,
5047 renderState: RenderState,
5048 ): void {
5043 - const idPrefix = resumableState.idPrefix;
5044 - const shellId = '\u00AB' + idPrefix + 'R\u00BB';
5045 - writeChunk(destination, blockingRenderChunkStart);
5046 - writeChunk(destination, stringToChunk(escapeTextForBrowser(shellId)));
5047 - writeChunk(destination, blockingRenderChunkEnd);
5049 + if (enableFizzBlockingRender) {
5050 + const idPrefix = resumableState.idPrefix;
5051 + const shellId = '\u00AB' + idPrefix + 'R\u00BB';
5052 + writeChunk(destination, blockingRenderChunkStart);
5053 + writeChunk(destination, stringToChunk(escapeTextForBrowser(shellId)));
5054 + writeChunk(destination, blockingRenderChunkEnd);
5055 + }
5056 }
5057
5058 const completedShellIdAttributeStart = stringToPrecomputedChunk(' id="');
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+20 -3
@@ -3590,7 +3590,9 @@ describe('ReactDOMFizzServer', () => {
3590 (gate(flags => flags.shouldUseFizzExternalRuntime)
3591 ? '<script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>'
3592 : '') +
3593 - '<link rel="expect" href="#«R»" blocking="render">',
3593 + (gate(flags => flags.enableFizzBlockingRender)
3594 + ? '<link rel="expect" href="#«R»" blocking="render">'
3595 + : ''),
3596 );
3597 });
3598
@@ -4523,7 +4525,15 @@ describe('ReactDOMFizzServer', () => {
4525
4526 // the html should be as-is
4527 expect(document.documentElement.innerHTML).toEqual(
4526 - '<head><script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script><link rel="expect" href="#«R»" blocking="render"></head><body><p>hello world!</p><template id="«R»"></template></body>',
4528 + '<head><script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>' +
4529 + (gate(flags => flags.enableFizzBlockingRender)
4530 + ? '<link rel="expect" href="#«R»" blocking="render">'
4531 + : '') +
4532 + '</head><body><p>hello world!</p>' +
4533 + (gate(flags => flags.enableFizzBlockingRender)
4534 + ? '<template id="«R»"></template>'
4535 + : '') +
4536 + '</body>',
4537 );
4538 });
4539
@@ -6512,7 +6522,14 @@ describe('ReactDOMFizzServer', () => {
6522 (gate(flags => flags.shouldUseFizzExternalRuntime)
6523 ? '<script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>'
6524 : '') +
6515 - '<link rel="expect" href="#«R»" blocking="render"></head><body><script>try { foo() } catch (e) {} ;</script><template id="«R»"></template></body></html>',
6525 + (gate(flags => flags.enableFizzBlockingRender)
6526 + ? '<link rel="expect" href="#«R»" blocking="render">'
6527 + : '') +
6528 + '</head><body><script>try { foo() } catch (e) {} ;</script>' +
6529 + (gate(flags => flags.enableFizzBlockingRender)
6530 + ? '<template id="«R»"></template>'
6531 + : '') +
6532 + '</body></html>',
6533 );
6534 });
6535
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+18 -4
@@ -84,9 +84,15 @@ describe('ReactDOMFizzServerBrowser', () => {
84 ),
85 );
86 const result = await readResult(stream);
87 - expect(result).toMatchInlineSnapshot(
88 - `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
89 - );
87 + if (gate(flags => flags.enableFizzBlockingRender)) {
88 + expect(result).toMatchInlineSnapshot(
89 + `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
90 + );
91 + } else {
92 + expect(result).toMatchInlineSnapshot(
93 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
94 + );
95 + }
96 });
97
98 it('should emit bootstrap script src at the end', async () => {
@@ -529,7 +535,15 @@ describe('ReactDOMFizzServerBrowser', () => {
535
536 const result = await readResult(stream);
537 expect(result).toEqual(
532 - '<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/><title>foo</title></head><body>bar<template id="«R»"></template></body></html>',
538 + '<!DOCTYPE html><html><head>' +
539 + (gate(flags => flags.enableFizzBlockingRender)
540 + ? '<link rel="expect" href="#«R»" blocking="render"/>'
541 + : '') +
542 + '<title>foo</title></head><body>bar' +
543 + (gate(flags => flags.enableFizzBlockingRender)
544 + ? '<template id="«R»"></template>'
545 + : '') +
546 + '</body></html>',
547 );
548 });
549
packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js
+9 -3
@@ -71,8 +71,14 @@ describe('ReactDOMFizzServerEdge', () => {
71 setTimeout(resolve, 1);
72 });
73
74 - expect(result).toMatchInlineSnapshot(
75 - `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body><main>hello</main><template id="«R»"></template></body></html>"`,
76 - );
74 + if (gate(flags => flags.enableFizzBlockingRender)) {
75 + expect(result).toMatchInlineSnapshot(
76 + `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body><main>hello</main><template id="«R»"></template></body></html>"`,
77 + );
78 + } else {
79 + expect(result).toMatchInlineSnapshot(
80 + `"<!DOCTYPE html><html><head></head><body><main>hello</main></body></html>"`,
81 + );
82 + }
83 });
84 });
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+9 -3
@@ -78,9 +78,15 @@ describe('ReactDOMFizzServerNode', () => {
78 pipe(writable);
79 });
80 // with Float, we emit empty heads if they are elided when rendering <html>
81 - expect(output.result).toMatchInlineSnapshot(
82 - `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
83 - );
81 + if (gate(flags => flags.enableFizzBlockingRender)) {
82 + expect(output.result).toMatchInlineSnapshot(
83 + `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
84 + );
85 + } else {
86 + expect(output.result).toMatchInlineSnapshot(
87 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
88 + );
89 + }
90 });
91
92 it('should emit bootstrap script src at the end', async () => {
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+18 -5
@@ -195,9 +195,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
195 ),
196 );
197 const prelude = await readContent(result.prelude);
198 - expect(prelude).toMatchInlineSnapshot(
199 - `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
200 - );
198 + if (gate(flags => flags.enableFizzBlockingRender)) {
199 + expect(prelude).toMatchInlineSnapshot(
200 + `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
201 + );
202 + } else {
203 + expect(prelude).toMatchInlineSnapshot(
204 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
205 + );
206 + }
207 });
208
209 it('should emit bootstrap script src at the end', async () => {
@@ -1438,8 +1444,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
1444 expect(await readContent(content)).toBe(
1445 '<!DOCTYPE html><html lang="en"><head>' +
1446 '<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
1441 - '<link rel="expect" href="#«R»" blocking="render"/></head>' +
1442 - '<body>Hello<template id="«R»"></template></body></html>',
1447 + (gate(flags => flags.enableFizzBlockingRender)
1448 + ? '<link rel="expect" href="#«R»" blocking="render"/>'
1449 + : '') +
1450 + '</head>' +
1451 + '<body>Hello' +
1452 + (gate(flags => flags.enableFizzBlockingRender)
1453 + ? '<template id="«R»"></template>'
1454 + : '') +
1455 + '</body></html>',
1456 );
1457 });
1458
packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
+9 -3
@@ -63,9 +63,15 @@ describe('ReactDOMFizzStaticNode', () => {
63 </html>,
64 );
65 const prelude = await readContent(result.prelude);
66 - expect(prelude).toMatchInlineSnapshot(
67 - `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
68 - );
66 + if (gate(flags => flags.enableFizzBlockingRender)) {
67 + expect(prelude).toMatchInlineSnapshot(
68 + `"<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head><body>hello world<template id="«R»"></template></body></html>"`,
69 + );
70 + } else {
71 + expect(prelude).toMatchInlineSnapshot(
72 + `"<!DOCTYPE html><html><head></head><body>hello world</body></html>"`,
73 + );
74 + }
75 });
76
77 // @gate experimental
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+8 -2
@@ -704,8 +704,14 @@ describe('ReactDOMFloat', () => {
704 (gate(flags => flags.shouldUseFizzExternalRuntime)
705 ? '<script src="react-dom/unstable_server-external-runtime" async=""></script>'
706 : '') +
707 - '<link rel="expect" href="#«R»" blocking="render"/><title>foo</title></head>' +
708 - '<body>bar<template id="«R»"></template>',
707 + (gate(flags => flags.enableFizzBlockingRender)
708 + ? '<link rel="expect" href="#«R»" blocking="render"/>'
709 + : '') +
710 + '<title>foo</title></head>' +
711 + '<body>bar' +
712 + (gate(flags => flags.enableFizzBlockingRender)
713 + ? '<template id="«R»"></template>'
714 + : ''),
715 '</body></html>',
716 ]);
717 });
packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js
+9 -2
@@ -34,8 +34,15 @@ describe('ReactDOMFloat', () => {
34 );
35
36 expect(result).toEqual(
37 - '<html><head><meta charSet="utf-8"/><link rel="expect" href="#«R»" blocking="render"/>' +
38 - '<title>title</title><script src="foo"></script></head><template id="«R»"></template></html>',
37 + '<html><head><meta charSet="utf-8"/>' +
38 + (gate(flags => flags.enableFizzBlockingRender)
39 + ? '<link rel="expect" href="#«R»" blocking="render"/>'
40 + : '') +
41 + '<title>title</title><script src="foo"></script></head>' +
42 + (gate(flags => flags.enableFizzBlockingRender)
43 + ? '<template id="«R»"></template>'
44 + : '') +
45 + '</html>',
46 );
47 });
48 });
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+35 -9
@@ -78,14 +78,20 @@ describe('rendering React components at document', () => {
78 root = ReactDOMClient.hydrateRoot(testDocument, <Root hello="world" />);
79 });
80 expect(testDocument.body.innerHTML).toBe(
81 - 'Hello world' + '<template id="«R»"></template>',
81 + 'Hello world' +
82 + (gate(flags => flags.enableFizzBlockingRender)
83 + ? '<template id="«R»"></template>'
84 + : ''),
85 );
86
87 await act(() => {
88 root.render(<Root hello="moon" />);
89 });
90 expect(testDocument.body.innerHTML).toBe(
88 - 'Hello moon' + '<template id="«R»"></template>',
91 + 'Hello moon' +
92 + (gate(flags => flags.enableFizzBlockingRender)
93 + ? '<template id="«R»"></template>'
94 + : ''),
95 );
96
97 expect(body === testDocument.body).toBe(true);
@@ -112,7 +118,10 @@ describe('rendering React components at document', () => {
118 root = ReactDOMClient.hydrateRoot(testDocument, <Root />);
119 });
120 expect(testDocument.body.innerHTML).toBe(
115 - 'Hello world' + '<template id="«R»"></template>',
121 + 'Hello world' +
122 + (gate(flags => flags.enableFizzBlockingRender)
123 + ? '<template id="«R»"></template>'
124 + : ''),
125 );
126
127 const originalDocEl = testDocument.documentElement;
@@ -124,9 +133,15 @@ describe('rendering React components at document', () => {
133 expect(testDocument.firstChild).toBe(originalDocEl);
134 expect(testDocument.head).toBe(originalHead);
135 expect(testDocument.body).toBe(originalBody);
127 - expect(originalBody.innerHTML).toBe('<template id="«R»"></template>');
136 + expect(originalBody.innerHTML).toBe(
137 + gate(flags => flags.enableFizzBlockingRender)
138 + ? '<template id="«R»"></template>'
139 + : '',
140 + );
141 expect(originalHead.innerHTML).toBe(
129 - '<link rel="expect" href="#«R»" blocking="render">',
142 + gate(flags => flags.enableFizzBlockingRender)
143 + ? '<link rel="expect" href="#«R»" blocking="render">'
144 + : '',
145 );
146 });
147
@@ -166,7 +181,10 @@ describe('rendering React components at document', () => {
181 });
182
183 expect(testDocument.body.innerHTML).toBe(
169 - 'Hello world' + '<template id="«R»"></template>',
184 + 'Hello world' +
185 + (gate(flags => flags.enableFizzBlockingRender)
186 + ? '<template id="«R»"></template>'
187 + : ''),
188 );
189
190 await act(() => {
@@ -174,7 +192,9 @@ describe('rendering React components at document', () => {
192 });
193
194 expect(testDocument.body.innerHTML).toBe(
177 - '<template id="«R»"></template>' + 'Goodbye world',
195 + (gate(flags => flags.enableFizzBlockingRender)
196 + ? '<template id="«R»"></template>'
197 + : '') + 'Goodbye world',
198 );
199 });
200
@@ -205,7 +225,10 @@ describe('rendering React components at document', () => {
225 });
226
227 expect(testDocument.body.innerHTML).toBe(
208 - 'Hello world' + '<template id="«R»"></template>',
228 + 'Hello world' +
229 + (gate(flags => flags.enableFizzBlockingRender)
230 + ? '<template id="«R»"></template>'
231 + : ''),
232 );
233 });
234
@@ -341,7 +364,10 @@ describe('rendering React components at document', () => {
364 expect(testDocument.body.innerHTML).toBe(
365 favorSafetyOverHydrationPerf
366 ? 'Hello world'
344 - : 'Goodbye world<template id="«R»"></template>',
367 + : 'Goodbye world' +
368 + (gate(flags => flags.enableFizzBlockingRender)
369 + ? '<template id="«R»"></template>'
370 + : ''),
371 );
372 });
373
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+18 -4
@@ -1921,14 +1921,28 @@ describe('ReactFlightDOM', () => {
1921 expect(content1).toEqual(
1922 '<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
1923 '<link rel="preload" href="after1" as="style"/>' +
1924 - '<link rel="expect" href="#«R»" blocking="render"/></head>' +
1925 - '<body><p>hello world</p><template id="«R»"></template></body></html>',
1924 + (gate(flags => flags.enableFizzBlockingRender)
1925 + ? '<link rel="expect" href="#«R»" blocking="render"/>'
1926 + : '') +
1927 + '</head>' +
1928 + '<body><p>hello world</p>' +
1929 + (gate(flags => flags.enableFizzBlockingRender)
1930 + ? '<template id="«R»"></template>'
1931 + : '') +
1932 + '</body></html>',
1933 );
1934 expect(content2).toEqual(
1935 '<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
1936 '<link rel="preload" href="after2" as="style"/>' +
1930 - '<link rel="expect" href="#«R»" blocking="render"/></head>' +
1931 - '<body><p>hello world</p><template id="«R»"></template></body></html>',
1937 + (gate(flags => flags.enableFizzBlockingRender)
1938 + ? '<link rel="expect" href="#«R»" blocking="render"/>'
1939 + : '') +
1940 + '</head>' +
1941 + '<body><p>hello world</p>' +
1942 + (gate(flags => flags.enableFizzBlockingRender)
1943 + ? '<template id="«R»"></template>'
1944 + : '') +
1945 + '</body></html>',
1946 );
1947 });
1948
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+10 -2
@@ -1899,8 +1899,16 @@ describe('ReactFlightDOMBrowser', () => {
1899 }
1900
1901 expect(content).toEqual(
1902 - '<!DOCTYPE html><html><head><link rel="expect" href="#«R»" blocking="render"/></head>' +
1903 - '<body><p>hello world</p><template id="«R»"></template></body></html>',
1902 + '<!DOCTYPE html><html><head>' +
1903 + (gate(flags => flags.enableFizzBlockingRender)
1904 + ? '<link rel="expect" href="#«R»" blocking="render"/>'
1905 + : '') +
1906 + '</head>' +
1907 + '<body><p>hello world</p>' +
1908 + (gate(flags => flags.enableFizzBlockingRender)
1909 + ? '<template id="«R»"></template>'
1910 + : '') +
1911 + '</body></html>',
1912 );
1913 });
1914
packages/shared/ReactFeatureFlags.js
+2
@@ -98,6 +98,8 @@ export const enableScrollEndPolyfill = __EXPERIMENTAL__;
98
99 export const enableSuspenseyImages = false;
100
101 +export const enableFizzBlockingRender = __EXPERIMENTAL__; // rel="expect"
102 +
103 export const enableSrcObject = __EXPERIMENTAL__;
104
105 export const enableHydrationChangeEvent = __EXPERIMENTAL__;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -83,6 +83,7 @@ export const enableViewTransition = false;
83 export const enableGestureTransition = false;
84 export const enableScrollEndPolyfill = true;
85 export const enableSuspenseyImages = false;
86 +export const enableFizzBlockingRender = true;
87 export const enableSrcObject = false;
88 export const enableHydrationChangeEvent = true;
89 export const enableDefaultTransitionIndicator = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -73,6 +73,7 @@ export const enableFastAddPropertiesInDiffing = false;
73 export const enableLazyPublicInstanceInFabric = false;
74 export const enableScrollEndPolyfill = true;
75 export const enableSuspenseyImages = false;
76 +export const enableFizzBlockingRender = true;
77 export const enableSrcObject = false;
78 export const enableHydrationChangeEvent = false;
79 export const enableDefaultTransitionIndicator = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -73,6 +73,7 @@ export const enableFastAddPropertiesInDiffing = true;
73 export const enableLazyPublicInstanceInFabric = false;
74 export const enableScrollEndPolyfill = true;
75 export const enableSuspenseyImages = false;
76 +export const enableFizzBlockingRender = true;
77 export const enableSrcObject = false;
78 export const enableHydrationChangeEvent = false;
79 export const enableDefaultTransitionIndicator = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -70,6 +70,7 @@ export const enableFastAddPropertiesInDiffing = false;
70 export const enableLazyPublicInstanceInFabric = false;
71 export const enableScrollEndPolyfill = true;
72 export const enableSuspenseyImages = false;
73 +export const enableFizzBlockingRender = true;
74 export const enableSrcObject = false;
75 export const enableHydrationChangeEvent = false;
76 export const enableDefaultTransitionIndicator = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -84,6 +84,7 @@ export const enableFastAddPropertiesInDiffing = false;
84 export const enableLazyPublicInstanceInFabric = false;
85 export const enableScrollEndPolyfill = true;
86 export const enableSuspenseyImages = false;
87 +export const enableFizzBlockingRender = true;
88 export const enableSrcObject = false;
89 export const enableHydrationChangeEvent = false;
90 export const enableDefaultTransitionIndicator = false;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -114,6 +114,7 @@ export const enableLazyPublicInstanceInFabric = false;
114 export const enableGestureTransition = false;
115
116 export const enableSuspenseyImages = false;
117 +export const enableFizzBlockingRender = true;
118 export const enableSrcObject = false;
119 export const enableHydrationChangeEvent = false;
120 export const enableDefaultTransitionIndicator = false;