Skip to content

Commit b94603b

Browse files
authored
[Fizz] Gate rel="expect" behind enableFizzBlockingRender (facebook#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.
1 parent 2bcf06b commit b94603b

19 files changed

+194
-55
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {Children} from 'react';
3434
import {
3535
enableFizzExternalRuntime,
3636
enableSrcObject,
37+
enableFizzBlockingRender,
3738
} from 'shared/ReactFeatureFlags';
3839

3940
import type {
@@ -4146,16 +4147,21 @@ export function writeCompletedRoot(
41464147
// we need to track the paint time of the shell so we know how much to throttle the reveal.
41474148
writeShellTimeInstruction(destination, resumableState, renderState);
41484149
}
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+
}
41594165
}
41604166
}
41614167
return writeBootstrap(destination, renderState);
@@ -5040,11 +5046,13 @@ function writeBlockingRenderInstruction(
50405046
resumableState: ResumableState,
50415047
renderState: RenderState,
50425048
): 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+
}
50485056
}
50495057

50505058
const completedShellIdAttributeStart = stringToPrecomputedChunk(' id="');

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3590,7 +3590,9 @@ describe('ReactDOMFizzServer', () => {
35903590
(gate(flags => flags.shouldUseFizzExternalRuntime)
35913591
? '<script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>'
35923592
: '') +
3593-
'<link rel="expect" href="#«R»" blocking="render">',
3593+
(gate(flags => flags.enableFizzBlockingRender)
3594+
? '<link rel="expect" href="#«R»" blocking="render">'
3595+
: ''),
35943596
);
35953597
});
35963598

@@ -4523,7 +4525,15 @@ describe('ReactDOMFizzServer', () => {
45234525

45244526
// the html should be as-is
45254527
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>',
45274537
);
45284538
});
45294539

@@ -6512,7 +6522,14 @@ describe('ReactDOMFizzServer', () => {
65126522
(gate(flags => flags.shouldUseFizzExternalRuntime)
65136523
? '<script src="react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js" async=""></script>'
65146524
: '') +
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>',
65166533
);
65176534
});
65186535

packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,15 @@ describe('ReactDOMFizzServerBrowser', () => {
8484
),
8585
);
8686
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+
}
9096
});
9197

9298
it('should emit bootstrap script src at the end', async () => {
@@ -529,7 +535,15 @@ describe('ReactDOMFizzServerBrowser', () => {
529535

530536
const result = await readResult(stream);
531537
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>',
533547
);
534548
});
535549

packages/react-dom/src/__tests__/ReactDOMFizzServerEdge-test.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,14 @@ describe('ReactDOMFizzServerEdge', () => {
7171
setTimeout(resolve, 1);
7272
});
7373

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+
}
7783
});
7884
});

packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,15 @@ describe('ReactDOMFizzServerNode', () => {
7878
pipe(writable);
7979
});
8080
// 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+
}
8490
});
8591

8692
it('should emit bootstrap script src at the end', async () => {

packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
195195
),
196196
);
197197
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+
}
201207
});
202208

203209
it('should emit bootstrap script src at the end', async () => {
@@ -1438,8 +1444,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
14381444
expect(await readContent(content)).toBe(
14391445
'<!DOCTYPE html><html lang="en"><head>' +
14401446
'<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>',
14431456
);
14441457
});
14451458

packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,15 @@ describe('ReactDOMFizzStaticNode', () => {
6363
</html>,
6464
);
6565
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+
}
6975
});
7076

7177
// @gate experimental

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -704,8 +704,14 @@ describe('ReactDOMFloat', () => {
704704
(gate(flags => flags.shouldUseFizzExternalRuntime)
705705
? '<script src="react-dom/unstable_server-external-runtime" async=""></script>'
706706
: '') +
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+
: ''),
709715
'</body></html>',
710716
]);
711717
});

packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,15 @@ describe('ReactDOMFloat', () => {
3434
);
3535

3636
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>',
3946
);
4047
});
4148
});

packages/react-dom/src/__tests__/ReactRenderDocument-test.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,20 @@ describe('rendering React components at document', () => {
7878
root = ReactDOMClient.hydrateRoot(testDocument, <Root hello="world" />);
7979
});
8080
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+
: ''),
8285
);
8386

8487
await act(() => {
8588
root.render(<Root hello="moon" />);
8689
});
8790
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+
: ''),
8995
);
9096

9197
expect(body === testDocument.body).toBe(true);
@@ -112,7 +118,10 @@ describe('rendering React components at document', () => {
112118
root = ReactDOMClient.hydrateRoot(testDocument, <Root />);
113119
});
114120
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+
: ''),
116125
);
117126

118127
const originalDocEl = testDocument.documentElement;
@@ -124,9 +133,15 @@ describe('rendering React components at document', () => {
124133
expect(testDocument.firstChild).toBe(originalDocEl);
125134
expect(testDocument.head).toBe(originalHead);
126135
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+
);
128141
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+
: '',
130145
);
131146
});
132147

@@ -166,15 +181,20 @@ describe('rendering React components at document', () => {
166181
});
167182

168183
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+
: ''),
170188
);
171189

172190
await act(() => {
173191
root.render(<Component2 />);
174192
});
175193

176194
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',
178198
);
179199
});
180200

@@ -205,7 +225,10 @@ describe('rendering React components at document', () => {
205225
});
206226

207227
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+
: ''),
209232
);
210233
});
211234

@@ -341,7 +364,10 @@ describe('rendering React components at document', () => {
341364
expect(testDocument.body.innerHTML).toBe(
342365
favorSafetyOverHydrationPerf
343366
? 'Hello world'
344-
: 'Goodbye world<template id="«R»"></template>',
367+
: 'Goodbye world' +
368+
(gate(flags => flags.enableFizzBlockingRender)
369+
? '<template id="«R»"></template>'
370+
: ''),
345371
);
346372
});
347373

0 commit comments

Comments
 (0)