From 5910eb34567a8699d1faa73b546baafd94f26411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 26 Mar 2024 19:52:46 -0700 Subject: [PATCH] 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. --- .../src/__tests__/ReactDOMFizzServer-test.js | 2 + .../src/__tests__/ReactDOMFloat-test.js | 2 + .../__tests__/ReactDOMHydrationDiff-test.js | 189 ++++++++++++------ ...DOMServerPartialHydration-test.internal.js | 2 + .../src/__tests__/ReactRenderDocument-test.js | 29 ++- .../ReactServerRenderingHydration-test.js | 53 +++-- .../ReactDOMServerIntegrationTestUtils.js | 8 +- .../src/ReactFiberHydrationContext.js | 5 +- packages/shared/ReactFeatureFlags.js | 1 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 2 + 15 files changed, 216 insertions(+), 82 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 1caaeca68d..5eea362a3e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -4423,6 +4423,7 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate favorSafetyOverHydrationPerf it('#24384: Suspending should halt hydration warnings but still emit hydration warnings after unsuspending if mismatches are genuine', async () => { const makeApp = () => { let resolve, resolved; @@ -4506,6 +4507,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([]); }); + // @gate favorSafetyOverHydrationPerf it('only warns once on hydration mismatch while within a suspense boundary', async () => { const originalConsoleError = console.error; const mockError = jest.fn(); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 7b8e27654c..b0ea66c592 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -6446,6 +6446,7 @@ body { ); }); + // @gate favorSafetyOverHydrationPerf it('retains styles even when a new html, head, and/body mount', async () => { await act(() => { const {pipe} = renderToPipeableStream( @@ -8230,6 +8231,7 @@ background-color: green; ]); }); + // @gate favorSafetyOverHydrationPerf it('can render a title before a singleton even if that singleton clears its contents', async () => { await act(() => { const {pipe} = renderToPipeableStream( diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index a52bb65181..f05332d6f1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -80,30 +80,55 @@ describe('ReactDOMServerHydration', () => { ); } - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "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: + if (gate(flags => flags.favorSafetyOverHydrationPerf)) { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", + "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: - - A server/client branch \`if (typeof window !== 'undefined')\`. - - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. - - Date formatting in a user's locale which doesn't match the server. - - External changing data without sending a snapshot of it along with the HTML. - - Invalid HTML tag nesting. + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. - It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. - https://react.dev/link/hydration-mismatch + https://react.dev/link/hydration-mismatch - -
-
- + client - - server - ]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + +
+
+ + client + - server + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); + } else { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + + +
+
+ + client + - server + ", + ] + `); + } }); // @gate __DEV__ @@ -120,29 +145,53 @@ describe('ReactDOMServerHydration', () => { } /* eslint-disable no-irregular-whitespace */ - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "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: + if (gate(flags => flags.favorSafetyOverHydrationPerf)) { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", + "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: - - A server/client branch \`if (typeof window !== 'undefined')\`. - - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. - - Date formatting in a user's locale which doesn't match the server. - - External changing data without sending a snapshot of it along with the HTML. - - Invalid HTML tag nesting. + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. - It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. - https://react.dev/link/hydration-mismatch + https://react.dev/link/hydration-mismatch - -
- + This markup contains an nbsp entity:   client text - - This markup contains an nbsp entity:   server text - ]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + +
+ + This markup contains an nbsp entity:   client text + - This markup contains an nbsp entity:   server text + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); + } else { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + + +
+ + This markup contains an nbsp entity:   client text + - This markup contains an nbsp entity:   server text + ", + ] + `); + } /* eslint-enable no-irregular-whitespace */ }); @@ -549,29 +598,53 @@ describe('ReactDOMServerHydration', () => { function Mismatch({isClient}) { return
{isClient && 'only'}
; } - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "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: + if (gate(flags => flags.favorSafetyOverHydrationPerf)) { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", + "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: - - A server/client branch \`if (typeof window !== 'undefined')\`. - - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. - - Date formatting in a user's locale which doesn't match the server. - - External changing data without sending a snapshot of it along with the HTML. - - Invalid HTML tag nesting. + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. - It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. - https://react.dev/link/hydration-mismatch + https://react.dev/link/hydration-mismatch - -
- + only - - - ]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + +
+ + only + - + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); + } else { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + + +
+ + only + - + ", + ] + `); + } }); // @gate __DEV__ diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 0953b0b353..d27f572968 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -3816,6 +3816,7 @@ describe('ReactDOMServerPartialHydration', () => { ); }); + // @gate favorSafetyOverHydrationPerf it("falls back to client rendering when there's a text mismatch (direct text child)", async () => { function DirectTextChild({text}) { return
{text}
; @@ -3845,6 +3846,7 @@ describe('ReactDOMServerPartialHydration', () => { ]); }); + // @gate favorSafetyOverHydrationPerf it("falls back to client rendering when there's a text mismatch (text child with siblings)", async () => { function Sibling() { return 'Sibling'; diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 238cc420ad..8467ceb3a1 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -276,6 +276,9 @@ describe('rendering React components at document', () => { ); const testDocument = getTestDocument(markup); + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); expect(() => { ReactDOM.flushSync(() => { ReactDOMClient.hydrateRoot( @@ -291,19 +294,29 @@ describe('rendering React components at document', () => { ); }); }).toErrorDev( - [ - 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], { withoutStack: 1, }, ); - assertLog([ - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", - 'Log recoverable error: There was an error while hydrating.', - ]); - expect(testDocument.body.innerHTML).toBe('Hello world'); + assertLog( + favorSafetyOverHydrationPerf + ? [ + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Log recoverable error: There was an error while hydrating.', + ] + : [], + ); + expect(testDocument.body.innerHTML).toBe( + favorSafetyOverHydrationPerf ? 'Hello world' : 'Goodbye world', + ); }); it('should render w/ no markup to full document', async () => { diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 5f234795e1..4772be4f01 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -123,6 +123,9 @@ describe('ReactDOMServerHydration', () => { // Now simulate a situation where the app is not idempotent. React should // warn but do the right thing. element.innerHTML = lastMarkup; + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); await expect(async () => { root = await act(() => { return ReactDOMClient.hydrateRoot( @@ -139,14 +142,22 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + " A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], {withoutStack: 1}, ); expect(mountCount).toEqual(4); expect(element.innerHTML.length > 0).toBe(true); - expect(element.innerHTML).not.toEqual(lastMarkup); + if (favorSafetyOverHydrationPerf) { + expect(element.innerHTML).not.toEqual(lastMarkup); + } else { + expect(element.innerHTML).toEqual(lastMarkup); + } // Ensure the events system works after markup mismatch. expect(numClicks).toEqual(1); @@ -212,6 +223,9 @@ describe('ReactDOMServerHydration', () => { const onFocusAfterHydration = jest.fn(); element.firstChild.focus = onFocusBeforeHydration; + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); await expect(async () => { await act(() => { ReactDOMClient.hydrateRoot( @@ -223,9 +237,13 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], {withoutStack: 1}, ); @@ -514,6 +532,9 @@ describe('ReactDOMServerHydration', () => { ); domElement.innerHTML = markup; + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); await expect(async () => { await act(() => { ReactDOMClient.hydrateRoot( @@ -524,14 +545,22 @@ describe('ReactDOMServerHydration', () => { {onRecoverableError: error => {}}, ); }); - - expect(domElement.innerHTML).not.toEqual(markup); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + " A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], {withoutStack: 1}, ); + + if (favorSafetyOverHydrationPerf) { + expect(domElement.innerHTML).not.toEqual(markup); + } else { + expect(domElement.innerHTML).toEqual(markup); + } }); it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => { diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js index 2f872492fa..d882e52e08 100644 --- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js +++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js @@ -17,10 +17,12 @@ module.exports = function (initModules) { let ReactDOMClient; let ReactDOMServer; let act; + let ReactFeatureFlags; function resetModules() { ({ReactDOM, ReactDOMClient, ReactDOMServer} = initModules()); act = require('internal-test-utils').act; + ReactFeatureFlags = require('shared/ReactFeatureFlags'); } function shouldUseDocument(reactElement) { @@ -276,8 +278,10 @@ module.exports = function (initModules) { const cleanTextContent = (cleanContainer.lastChild && cleanContainer.lastChild.textContent) || ''; - // The only guarantee is that text content has been patched up if needed. - expect(hydratedTextContent).toBe(cleanTextContent); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + // The only guarantee is that text content has been patched up if needed. + expect(hydratedTextContent).toBe(cleanTextContent); + } // Abort any further expects. All bets are off at this point. throw new BadMarkupExpected(); diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index 2ddc7d2514..ce7f4be96e 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -27,6 +27,7 @@ import { HostRoot, SuspenseComponent, } from './ReactWorkTags'; +import {favorSafetyOverHydrationPerf} from 'shared/ReactFeatureFlags'; import {createFiberFromDehydratedFragment} from './ReactFiber'; import { @@ -472,7 +473,7 @@ function prepareToHydrateHostInstance( hostContext, fiber, ); - if (!didHydrate) { + if (!didHydrate && favorSafetyOverHydrationPerf) { throwOnHydrationMismatch(fiber); } } @@ -538,7 +539,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void { fiber, parentProps, ); - if (!didHydrate) { + if (!didHydrate && favorSafetyOverHydrationPerf) { throwOnHydrationMismatch(fiber); } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index fc4c241c2a..065391c504 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -30,6 +30,7 @@ export const enableComponentStackLocations = true; // ----------------------------------------------------------------------------- // TODO: Finish rolling out in www +export const favorSafetyOverHydrationPerf = true; export const enableAsyncActions = true; // Need to remove didTimeout argument from Scheduler before landing diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 642af8be74..7231aa51da 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -65,6 +65,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = true; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index c03dd5ac12..bb771e6560 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -87,6 +87,7 @@ export const disableTextareaChildren = false; export const enableSuspenseAvoidThisFallback = false; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index d1143a36ba..b4bbcc58fc 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -39,6 +39,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index b61b27ea3a..f6b042b852 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -45,6 +45,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableUseRefAccessWarning = false; export const enableInfiniteRenderLoopDetection = false; export const enableRenderableContext = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 7cb09b6a61..418c3389f8 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -41,6 +41,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index fc839d73ce..54ce253de7 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -60,6 +60,8 @@ export const enableUseEffectEventHook = true; export const enableFilterEmptyStringAttributesDOM = true; export const enableAsyncActions = true; +export const favorSafetyOverHydrationPerf = false; + // Logs additional User Timing API marks for use with an experimental profiling tool. export const enableSchedulingProfiler: boolean = __PROFILE__ && dynamicFeatureFlags.enableSchedulingProfiler;