@samitouri / QOS-React / commits / 5910eb3456

Add Flag to Favor Hydration Performance over User Safety (#28655)

If false, this ignores text comparison checks during hydration at the risk of privacy safety. Since React 18 we recreate the DOM starting from the nearest Suspense boundary if any of the text content mismatches. This ensures that if we have nodes that otherwise line up correctly such as if they're the same type of Component but in a different order, then we don't accidentally transfer state or attributes to the wrong one. If we didn't do this e.g. attributes like image src might not line up with the text. E.g. you might show the wrong profile picture with the wrong name. However, the main reason we do this is because it's a security/privacy concern if state from the original node can transfer to the other one. For example if you start typing into a text field to reply to a story but then it turns out that the hydration was in a different order, you might submit that text into a different story than you intended. Similarly, if you've already clicked an item and that gets replayed using Action replaying or is synchronously force hydrated - that click might end up applying to a different item in the list than you intended. E.g. liking the wrong photo. Unfortunately a common case where this happens is when Google Translate is applied to a page. It'll always cause mismatches and recreate the tree. Most of the time this wouldn't be visible to users because it'd just recreate to the same thing and then translate again. It can affect metrics that trace when this hydration happened though. Meta can use this flag to decide if they favor this perf metric over the risk to user privacy. This is similar to the old enableClientRenderFallbackOnTextMismatch flag except this flag doesn't patch up the text when there's a mismatch. Because we don't have the patching anymore. The assumption is that it is safe to ignore the safety concern because we assume it's a match and therefore favoring not patching it will lead to better perf.

Sebastian Markbåge committed Mar 26, 2024 at 19:52 UTC 5910eb34567a8699d1faa73b546baafd94f26411
15 files changed +216 -82
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+2
@@ -4423,6 +4423,7 @@ describe('ReactDOMFizzServer', () => {
4423 );
4424 });
4425
4426 + // @gate favorSafetyOverHydrationPerf
4427 it('#24384: Suspending should halt hydration warnings but still emit hydration warnings after unsuspending if mismatches are genuine', async () => {
4428 const makeApp = () => {
4429 let resolve, resolved;
@@ -4506,6 +4507,7 @@ describe('ReactDOMFizzServer', () => {
4507 await waitForAll([]);
4508 });
4509
4510 + // @gate favorSafetyOverHydrationPerf
4511 it('only warns once on hydration mismatch while within a suspense boundary', async () => {
4512 const originalConsoleError = console.error;
4513 const mockError = jest.fn();
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+2
@@ -6446,6 +6446,7 @@ body {
6446 );
6447 });
6448
6449 + // @gate favorSafetyOverHydrationPerf
6450 it('retains styles even when a new html, head, and/body mount', async () => {
6451 await act(() => {
6452 const {pipe} = renderToPipeableStream(
@@ -8230,6 +8231,7 @@ background-color: green;
8231 ]);
8232 });
8233
8234 + // @gate favorSafetyOverHydrationPerf
8235 it('can render a title before a singleton even if that singleton clears its contents', async () => {
8236 await act(() => {
8237 const {pipe} = renderToPipeableStream(
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+131 -58
@@ -80,30 +80,55 @@ describe('ReactDOMServerHydration', () => {
80 </div>
81 );
82 }
83 - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
84 - [
85 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
86 - "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
83 + if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
84 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
85 + [
86 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
87 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
88 +
89 + - A server/client branch \`if (typeof window !== 'undefined')\`.
90 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
91 + - Date formatting in a user's locale which doesn't match the server.
92 + - External changing data without sending a snapshot of it along with the HTML.
93 + - Invalid HTML tag nesting.
94 +
95 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
96 +
97 + https://react.dev/link/hydration-mismatch
98 +
99 + <Mismatch isClient={true}>
100 + <div className="parent">
101 + <main className="child">
102 + + client
103 + - server
104 + ]",
105 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
106 + ]
107 + `);
108 + } else {
109 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
110 + [
111 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
112
88 - - A server/client branch \`if (typeof window !== 'undefined')\`.
89 - - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
90 - - Date formatting in a user's locale which doesn't match the server.
91 - - External changing data without sending a snapshot of it along with the HTML.
92 - - Invalid HTML tag nesting.
113 + - A server/client branch \`if (typeof window !== 'undefined')\`.
114 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
115 + - Date formatting in a user's locale which doesn't match the server.
116 + - External changing data without sending a snapshot of it along with the HTML.
117 + - Invalid HTML tag nesting.
118
94 - It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
119 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
120
96 - https://react.dev/link/hydration-mismatch
121 + https://react.dev/link/hydration-mismatch
122
98 - <Mismatch isClient={true}>
99 - <div className="parent">
100 - <main className="child">
101 - + client
102 - - server
103 - ]",
104 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
105 - ]
106 - `);
123 + <Mismatch isClient={true}>
124 + <div className="parent">
125 + <main className="child">
126 + + client
127 + - server
128 + ",
129 + ]
130 + `);
131 + }
132 });
133
134 // @gate __DEV__
@@ -120,29 +145,53 @@ describe('ReactDOMServerHydration', () => {
145 }
146
147 /* eslint-disable no-irregular-whitespace */
123 - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
124 - [
125 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
126 - "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
148 + if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
149 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
150 + [
151 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
152 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
153
128 - - A server/client branch \`if (typeof window !== 'undefined')\`.
129 - - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
130 - - Date formatting in a user's locale which doesn't match the server.
131 - - External changing data without sending a snapshot of it along with the HTML.
132 - - Invalid HTML tag nesting.
154 + - A server/client branch \`if (typeof window !== 'undefined')\`.
155 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
156 + - Date formatting in a user's locale which doesn't match the server.
157 + - External changing data without sending a snapshot of it along with the HTML.
158 + - Invalid HTML tag nesting.
159
134 - It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
160 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
161
136 - https://react.dev/link/hydration-mismatch
162 + https://react.dev/link/hydration-mismatch
163
138 - <Mismatch isClient={true}>
139 - <div>
140 - + This markup contains an nbsp entity:   client text
141 - - This markup contains an nbsp entity:   server text
142 - ]",
143 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
144 - ]
145 - `);
164 + <Mismatch isClient={true}>
165 + <div>
166 + + This markup contains an nbsp entity:   client text
167 + - This markup contains an nbsp entity:   server text
168 + ]",
169 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
170 + ]
171 + `);
172 + } else {
173 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
174 + [
175 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
176 +
177 + - A server/client branch \`if (typeof window !== 'undefined')\`.
178 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
179 + - Date formatting in a user's locale which doesn't match the server.
180 + - External changing data without sending a snapshot of it along with the HTML.
181 + - Invalid HTML tag nesting.
182 +
183 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
184 +
185 + https://react.dev/link/hydration-mismatch
186 +
187 + <Mismatch isClient={true}>
188 + <div>
189 + + This markup contains an nbsp entity:   client text
190 + - This markup contains an nbsp entity:   server text
191 + ",
192 + ]
193 + `);
194 + }
195 /* eslint-enable no-irregular-whitespace */
196 });
197
@@ -549,29 +598,53 @@ describe('ReactDOMServerHydration', () => {
598 function Mismatch({isClient}) {
599 return <div className="parent">{isClient && 'only'}</div>;
600 }
552 - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
553 - [
554 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
555 - "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
601 + if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
602 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
603 + [
604 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
605 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
606 +
607 + - A server/client branch \`if (typeof window !== 'undefined')\`.
608 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
609 + - Date formatting in a user's locale which doesn't match the server.
610 + - External changing data without sending a snapshot of it along with the HTML.
611 + - Invalid HTML tag nesting.
612 +
613 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
614 +
615 + https://react.dev/link/hydration-mismatch
616 +
617 + <Mismatch isClient={true}>
618 + <div className="parent">
619 + + only
620 + -
621 + ]",
622 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
623 + ]
624 + `);
625 + } else {
626 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
627 + [
628 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
629
557 - - A server/client branch \`if (typeof window !== 'undefined')\`.
558 - - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
559 - - Date formatting in a user's locale which doesn't match the server.
560 - - External changing data without sending a snapshot of it along with the HTML.
561 - - Invalid HTML tag nesting.
630 + - A server/client branch \`if (typeof window !== 'undefined')\`.
631 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
632 + - Date formatting in a user's locale which doesn't match the server.
633 + - External changing data without sending a snapshot of it along with the HTML.
634 + - Invalid HTML tag nesting.
635
563 - It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
636 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
637
565 - https://react.dev/link/hydration-mismatch
638 + https://react.dev/link/hydration-mismatch
639
567 - <Mismatch isClient={true}>
568 - <div className="parent">
569 - + only
570 - -
571 - ]",
572 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
573 - ]
574 - `);
640 + <Mismatch isClient={true}>
641 + <div className="parent">
642 + + only
643 + -
644 + ",
645 + ]
646 + `);
647 + }
648 });
649
650 // @gate __DEV__
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+2
@@ -3816,6 +3816,7 @@ describe('ReactDOMServerPartialHydration', () => {
3816 );
3817 });
3818
3819 + // @gate favorSafetyOverHydrationPerf
3820 it("falls back to client rendering when there's a text mismatch (direct text child)", async () => {
3821 function DirectTextChild({text}) {
3822 return <div>{text}</div>;
@@ -3845,6 +3846,7 @@ describe('ReactDOMServerPartialHydration', () => {
3846 ]);
3847 });
3848
3849 + // @gate favorSafetyOverHydrationPerf
3850 it("falls back to client rendering when there's a text mismatch (text child with siblings)", async () => {
3851 function Sibling() {
3852 return 'Sibling';
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+21 -8
@@ -276,6 +276,9 @@ describe('rendering React components at document', () => {
276 );
277 const testDocument = getTestDocument(markup);
278
279 + const favorSafetyOverHydrationPerf = gate(
280 + flags => flags.favorSafetyOverHydrationPerf,
281 + );
282 expect(() => {
283 ReactDOM.flushSync(() => {
284 ReactDOMClient.hydrateRoot(
@@ -291,19 +294,29 @@ describe('rendering React components at document', () => {
294 );
295 });
296 }).toErrorDev(
294 - [
295 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
296 - ],
297 + favorSafetyOverHydrationPerf
298 + ? [
299 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
300 + ]
301 + : [
302 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
303 + ],
304 {
305 withoutStack: 1,
306 },
307 );
308
302 - assertLog([
303 - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
304 - 'Log recoverable error: There was an error while hydrating.',
305 - ]);
306 - expect(testDocument.body.innerHTML).toBe('Hello world');
309 + assertLog(
310 + favorSafetyOverHydrationPerf
311 + ? [
312 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
313 + 'Log recoverable error: There was an error while hydrating.',
314 + ]
315 + : [],
316 + );
317 + expect(testDocument.body.innerHTML).toBe(
318 + favorSafetyOverHydrationPerf ? 'Hello world' : 'Goodbye world',
319 + );
320 });
321
322 it('should render w/ no markup to full document', async () => {
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+41 -12
@@ -123,6 +123,9 @@ describe('ReactDOMServerHydration', () => {
123 // Now simulate a situation where the app is not idempotent. React should
124 // warn but do the right thing.
125 element.innerHTML = lastMarkup;
126 + const favorSafetyOverHydrationPerf = gate(
127 + flags => flags.favorSafetyOverHydrationPerf,
128 + );
129 await expect(async () => {
130 root = await act(() => {
131 return ReactDOMClient.hydrateRoot(
@@ -139,14 +142,22 @@ describe('ReactDOMServerHydration', () => {
142 );
143 });
144 }).toErrorDev(
142 - [
143 - 'An error occurred during hydration. The server HTML was replaced with client content.',
144 - ],
145 + favorSafetyOverHydrationPerf
146 + ? [
147 + 'An error occurred during hydration. The server HTML was replaced with client content.',
148 + ]
149 + : [
150 + " A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
151 + ],
152 {withoutStack: 1},
153 );
154 expect(mountCount).toEqual(4);
155 expect(element.innerHTML.length > 0).toBe(true);
149 - expect(element.innerHTML).not.toEqual(lastMarkup);
156 + if (favorSafetyOverHydrationPerf) {
157 + expect(element.innerHTML).not.toEqual(lastMarkup);
158 + } else {
159 + expect(element.innerHTML).toEqual(lastMarkup);
160 + }
161
162 // Ensure the events system works after markup mismatch.
163 expect(numClicks).toEqual(1);
@@ -212,6 +223,9 @@ describe('ReactDOMServerHydration', () => {
223 const onFocusAfterHydration = jest.fn();
224 element.firstChild.focus = onFocusBeforeHydration;
225
226 + const favorSafetyOverHydrationPerf = gate(
227 + flags => flags.favorSafetyOverHydrationPerf,
228 + );
229 await expect(async () => {
230 await act(() => {
231 ReactDOMClient.hydrateRoot(
@@ -223,9 +237,13 @@ describe('ReactDOMServerHydration', () => {
237 );
238 });
239 }).toErrorDev(
226 - [
227 - 'An error occurred during hydration. The server HTML was replaced with client content.',
228 - ],
240 + favorSafetyOverHydrationPerf
241 + ? [
242 + 'An error occurred during hydration. The server HTML was replaced with client content.',
243 + ]
244 + : [
245 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
246 + ],
247 {withoutStack: 1},
248 );
249
@@ -514,6 +532,9 @@ describe('ReactDOMServerHydration', () => {
532 );
533 domElement.innerHTML = markup;
534
535 + const favorSafetyOverHydrationPerf = gate(
536 + flags => flags.favorSafetyOverHydrationPerf,
537 + );
538 await expect(async () => {
539 await act(() => {
540 ReactDOMClient.hydrateRoot(
@@ -524,14 +545,22 @@ describe('ReactDOMServerHydration', () => {
545 {onRecoverableError: error => {}},
546 );
547 });
527 -
528 - expect(domElement.innerHTML).not.toEqual(markup);
548 }).toErrorDev(
530 - [
531 - 'An error occurred during hydration. The server HTML was replaced with client content.',
532 - ],
549 + favorSafetyOverHydrationPerf
550 + ? [
551 + 'An error occurred during hydration. The server HTML was replaced with client content.',
552 + ]
553 + : [
554 + " A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
555 + ],
556 {withoutStack: 1},
557 );
558 +
559 + if (favorSafetyOverHydrationPerf) {
560 + expect(domElement.innerHTML).not.toEqual(markup);
561 + } else {
562 + expect(domElement.innerHTML).toEqual(markup);
563 + }
564 });
565
566 it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => {
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+6 -2
@@ -17,10 +17,12 @@ module.exports = function (initModules) {
17 let ReactDOMClient;
18 let ReactDOMServer;
19 let act;
20 + let ReactFeatureFlags;
21
22 function resetModules() {
23 ({ReactDOM, ReactDOMClient, ReactDOMServer} = initModules());
24 act = require('internal-test-utils').act;
25 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
26 }
27
28 function shouldUseDocument(reactElement) {
@@ -276,8 +278,10 @@ module.exports = function (initModules) {
278 const cleanTextContent =
279 (cleanContainer.lastChild && cleanContainer.lastChild.textContent) || '';
280
279 - // The only guarantee is that text content has been patched up if needed.
280 - expect(hydratedTextContent).toBe(cleanTextContent);
281 + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) {
282 + // The only guarantee is that text content has been patched up if needed.
283 + expect(hydratedTextContent).toBe(cleanTextContent);
284 + }
285
286 // Abort any further expects. All bets are off at this point.
287 throw new BadMarkupExpected();
packages/react-reconciler/src/ReactFiberHydrationContext.js
+3 -2
@@ -27,6 +27,7 @@ import {
27 HostRoot,
28 SuspenseComponent,
29 } from './ReactWorkTags';
30 +import {favorSafetyOverHydrationPerf} from 'shared/ReactFeatureFlags';
31
32 import {createFiberFromDehydratedFragment} from './ReactFiber';
33 import {
@@ -472,7 +473,7 @@ function prepareToHydrateHostInstance(
473 hostContext,
474 fiber,
475 );
475 - if (!didHydrate) {
476 + if (!didHydrate && favorSafetyOverHydrationPerf) {
477 throwOnHydrationMismatch(fiber);
478 }
479 }
@@ -538,7 +539,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
539 fiber,
540 parentProps,
541 );
541 - if (!didHydrate) {
542 + if (!didHydrate && favorSafetyOverHydrationPerf) {
543 throwOnHydrationMismatch(fiber);
544 }
545 }
packages/shared/ReactFeatureFlags.js
+1
@@ -30,6 +30,7 @@ export const enableComponentStackLocations = true;
30 // -----------------------------------------------------------------------------
31
32 // TODO: Finish rolling out in www
33 +export const favorSafetyOverHydrationPerf = true;
34 export const enableAsyncActions = true;
35
36 // Need to remove didTimeout argument from Scheduler before landing
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -65,6 +65,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
65 export const enableCPUSuspense = true;
66 export const enableUseMemoCacheHook = true;
67 export const enableUseEffectEventHook = false;
68 +export const favorSafetyOverHydrationPerf = true;
69 export const enableLegacyFBSupport = false;
70 export const enableFilterEmptyStringAttributesDOM = true;
71 export const enableGetInspectorDataForInstanceInProduction = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -87,6 +87,7 @@ export const disableTextareaChildren = false;
87 export const enableSuspenseAvoidThisFallback = false;
88 export const enableSuspenseAvoidThisFallbackFizz = false;
89 export const enableUseEffectEventHook = false;
90 +export const favorSafetyOverHydrationPerf = true;
91 export const enableLegacyFBSupport = false;
92 export const enableFilterEmptyStringAttributesDOM = true;
93 export const enableGetInspectorDataForInstanceInProduction = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -39,6 +39,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
39 export const enableCPUSuspense = false;
40 export const enableUseMemoCacheHook = true;
41 export const enableUseEffectEventHook = false;
42 +export const favorSafetyOverHydrationPerf = true;
43 export const enableComponentStackLocations = true;
44 export const enableLegacyFBSupport = false;
45 export const enableFilterEmptyStringAttributesDOM = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -45,6 +45,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
45 export const enableCPUSuspense = false;
46 export const enableUseMemoCacheHook = true;
47 export const enableUseEffectEventHook = false;
48 +export const favorSafetyOverHydrationPerf = true;
49 export const enableUseRefAccessWarning = false;
50 export const enableInfiniteRenderLoopDetection = false;
51 export const enableRenderableContext = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -41,6 +41,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
41 export const enableCPUSuspense = false;
42 export const enableUseMemoCacheHook = true;
43 export const enableUseEffectEventHook = false;
44 +export const favorSafetyOverHydrationPerf = true;
45 export const enableComponentStackLocations = true;
46 export const enableLegacyFBSupport = false;
47 export const enableFilterEmptyStringAttributesDOM = true;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -60,6 +60,8 @@ export const enableUseEffectEventHook = true;
60 export const enableFilterEmptyStringAttributesDOM = true;
61 export const enableAsyncActions = true;
62
63 +export const favorSafetyOverHydrationPerf = false;
64 +
65 // Logs additional User Timing API marks for use with an experimental profiling tool.
66 export const enableSchedulingProfiler: boolean =
67 __PROFILE__ && dynamicFeatureFlags.enableSchedulingProfiler;