diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index cadf311d54ffd..7ae3941c9253a 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -471,3 +471,7 @@ export function afterActiveInstanceBlur() { export function preparePortalMount(portalInstance: any): void { // noop } + +export function detachDeletedInstance(node: Instance): void { + // noop +} diff --git a/packages/react-dom/src/client/ReactDOMComponentTree.js b/packages/react-dom/src/client/ReactDOMComponentTree.js index fbf0a81e29064..2afe66035f364 100644 --- a/packages/react-dom/src/client/ReactDOMComponentTree.js +++ b/packages/react-dom/src/client/ReactDOMComponentTree.js @@ -43,6 +43,16 @@ const internalEventHandlersKey = '__reactEvents$' + randomKey; const internalEventHandlerListenersKey = '__reactListeners$' + randomKey; const internalEventHandlesSetKey = '__reactHandles$' + randomKey; +export function detachDeletedInstance(node: Instance): void { + // TODO: This function is only called on host components. I don't think all of + // these fields are relevant. + delete (node: any)[internalInstanceKey]; + delete (node: any)[internalPropsKey]; + delete (node: any)[internalEventHandlersKey]; + delete (node: any)[internalEventHandlerListenersKey]; + delete (node: any)[internalEventHandlesSetKey]; +} + export function precacheFiberNode( hostInst: Fiber, node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance, diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 156951ac748bc..695242ca50dbb 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -25,6 +25,7 @@ import { getInstanceFromNode as getInstanceFromNodeDOMTree, isContainerMarkedAsRoot, } from './ReactDOMComponentTree'; +export {detachDeletedInstance} from './ReactDOMComponentTree'; import {hasRole} from './DOMAccessibilityRoles'; import { createElement, diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 736001a853e63..347ace3b79190 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -481,3 +481,7 @@ export function afterActiveInstanceBlur() { export function preparePortalMount(portalInstance: Instance): void { // noop } + +export function detachDeletedInstance(node: Instance): void { + // noop +} diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index d626a175d9340..84538532979e1 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -538,3 +538,7 @@ export function afterActiveInstanceBlur() { export function preparePortalMount(portalInstance: Instance): void { // noop } + +export function detachDeletedInstance(node: Instance): void { + // noop +} diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 80c888a88e53d..dd304fce6d878 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -420,6 +420,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { getInstanceFromScope() { throw new Error('Not yet implemented.'); }, + + detachDeletedInstance() {}, }; const hostConfig = useMutation diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index c20719d8bfd96..3bf3940074228 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -35,6 +35,7 @@ import { enableSuspenseCallback, enableScopeAPI, enableStrictEffects, + deletedTreeCleanUpLevel, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -60,6 +61,7 @@ import { hasCaughtError, clearCaughtError, } from 'shared/ReactErrorUtils'; +import {detachDeletedInstance} from './ReactFiberHostConfig'; import { NoFlags, ContentReset, @@ -1219,25 +1221,84 @@ function detachFiberMutation(fiber: Fiber) { // Don't reset the alternate yet, either. We need that so we can detach the // alternate's fields in the passive phase. Clearing the return pointer is // sufficient for findDOMNode semantics. + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.return = null; + } fiber.return = null; } -export function detachFiberAfterEffects(fiber: Fiber): void { - // Null out fields to improve GC for references that may be lingering (e.g. DevTools). - // Note that we already cleared the return pointer in detachFiberMutation(). - fiber.alternate = null; - fiber.child = null; - fiber.deletions = null; - fiber.dependencies = null; - fiber.memoizedProps = null; - fiber.memoizedState = null; - fiber.pendingProps = null; - fiber.sibling = null; - fiber.stateNode = null; - fiber.updateQueue = null; +function detachFiberAfterEffects(fiber: Fiber) { + const alternate = fiber.alternate; + if (alternate !== null) { + fiber.alternate = null; + detachFiberAfterEffects(alternate); + } + + // Note: Defensively using negation instead of < in case + // `deletedTreeCleanUpLevel` is undefined. + if (!(deletedTreeCleanUpLevel >= 2)) { + // This is the default branch (level 0). + fiber.child = null; + fiber.deletions = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.sibling = null; + fiber.stateNode = null; + fiber.updateQueue = null; - if (__DEV__) { - fiber._debugOwner = null; + if (__DEV__) { + fiber._debugOwner = null; + } + } else { + // Clear cyclical Fiber fields. This level alone is designed to roughly + // approximate the planned Fiber refactor. In that world, `setState` will be + // bound to a special "instance" object instead of a Fiber. The Instance + // object will not have any of these fields. It will only be connected to + // the fiber tree via a single link at the root. So if this level alone is + // sufficient to fix memory issues, that bodes well for our plans. + fiber.child = null; + fiber.deletions = null; + fiber.sibling = null; + + // I'm intentionally not clearing the `return` field in this level. We + // already disconnect the `return` pointer at the root of the deleted + // subtree (in `detachFiberMutation`). Besides, `return` by itself is not + // cyclical — it's only cyclical when combined with `child`, `sibling`, and + // `alternate`. But we'll clear it in the next level anyway, just in case. + + if (__DEV__) { + fiber._debugOwner = null; + } + + if (deletedTreeCleanUpLevel >= 3) { + // Theoretically, nothing in here should be necessary, because we already + // disconnected the fiber from the tree. So even if something leaks this + // particular fiber, it won't leak anything else + // + // The purpose of this branch is to be super aggressive so we can measure + // if there's any difference in memory impact. If there is, that could + // indicate a React leak we don't know about. + + // For host components, disconnect host instance -> fiber pointer. + if (fiber.tag === HostComponent) { + const hostInstance: Instance = fiber.stateNode; + if (hostInstance !== null) { + detachDeletedInstance(hostInstance); + } + } + + fiber.return = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.stateNode = null; + // TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead. + fiber.updateQueue = null; + } } } @@ -1629,11 +1690,8 @@ function commitDeletion( renderPriorityLevel, ); } - const alternate = current.alternate; + detachFiberMutation(current); - if (alternate !== null) { - detachFiberMutation(alternate); - } } function commitWork(current: Fiber | null, finishedWork: Fiber): void { @@ -2308,14 +2366,34 @@ function commitPassiveUnmountEffects_begin() { fiberToDelete, fiber, ); + } - // Now that passive effects have been processed, it's safe to detach lingering pointers. - const alternate = fiberToDelete.alternate; - detachFiberAfterEffects(fiberToDelete); - if (alternate !== null) { - detachFiberAfterEffects(alternate); + if (deletedTreeCleanUpLevel >= 1) { + // A fiber was deleted from this parent fiber, but it's still part of + // the previous (alternate) parent fiber's list of children. Because + // children are a linked list, an earlier sibling that's still alive + // will be connected to the deleted fiber via its `alternate`: + // + // live fiber + // --alternate--> previous live fiber + // --sibling--> deleted fiber + // + // We can't disconnect `alternate` on nodes that haven't been deleted + // yet, but we can disconnect the `sibling` and `child` pointers. + const previousFiber = fiber.alternate; + if (previousFiber !== null) { + let detachedChild = previousFiber.child; + if (detachedChild !== null) { + previousFiber.child = null; + do { + const detachedSibling = detachedChild.sibling; + detachedChild.sibling = null; + detachedChild = detachedSibling; + } while (detachedChild !== null); + } } } + nextEffect = fiber; } } @@ -2392,7 +2470,8 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin( resetCurrentDebugFiberInDEV(); const child = fiber.child; - // TODO: Only traverse subtree if it has a PassiveStatic flag + // TODO: Only traverse subtree if it has a PassiveStatic flag. (But, if we + // do this, still need to handle `deletedTreeCleanUpLevel` correctly.) if (child !== null) { ensureCorrectReturnPointer(child, fiber); nextEffect = child; @@ -2409,19 +2488,35 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete( ) { while (nextEffect !== null) { const fiber = nextEffect; - if (fiber === deletedSubtreeRoot) { - nextEffect = null; - return; + const sibling = fiber.sibling; + const returnFiber = fiber.return; + + if (deletedTreeCleanUpLevel >= 2) { + // Recursively traverse the entire deleted tree and clean up fiber fields. + // This is more aggressive than ideal, and the long term goal is to only + // have to detach the deleted tree at the root. + detachFiberAfterEffects(fiber); + if (fiber === deletedSubtreeRoot) { + nextEffect = null; + return; + } + } else { + // This is the default branch (level 0). We do not recursively clear all + // the fiber fields. Only the root of the deleted subtree. + if (fiber === deletedSubtreeRoot) { + detachFiberAfterEffects(fiber); + nextEffect = null; + return; + } } - const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + ensureCorrectReturnPointer(sibling, returnFiber); nextEffect = sibling; return; } - nextEffect = fiber.return; + nextEffect = returnFiber; } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index efdbc39abfdfc..7f4da55e76e2e 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -35,6 +35,7 @@ import { enableSuspenseCallback, enableScopeAPI, enableStrictEffects, + deletedTreeCleanUpLevel, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -60,6 +61,7 @@ import { hasCaughtError, clearCaughtError, } from 'shared/ReactErrorUtils'; +import {detachDeletedInstance} from './ReactFiberHostConfig'; import { NoFlags, ContentReset, @@ -1219,25 +1221,84 @@ function detachFiberMutation(fiber: Fiber) { // Don't reset the alternate yet, either. We need that so we can detach the // alternate's fields in the passive phase. Clearing the return pointer is // sufficient for findDOMNode semantics. + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.return = null; + } fiber.return = null; } -export function detachFiberAfterEffects(fiber: Fiber): void { - // Null out fields to improve GC for references that may be lingering (e.g. DevTools). - // Note that we already cleared the return pointer in detachFiberMutation(). - fiber.alternate = null; - fiber.child = null; - fiber.deletions = null; - fiber.dependencies = null; - fiber.memoizedProps = null; - fiber.memoizedState = null; - fiber.pendingProps = null; - fiber.sibling = null; - fiber.stateNode = null; - fiber.updateQueue = null; +function detachFiberAfterEffects(fiber: Fiber) { + const alternate = fiber.alternate; + if (alternate !== null) { + fiber.alternate = null; + detachFiberAfterEffects(alternate); + } + + // Note: Defensively using negation instead of < in case + // `deletedTreeCleanUpLevel` is undefined. + if (!(deletedTreeCleanUpLevel >= 2)) { + // This is the default branch (level 0). + fiber.child = null; + fiber.deletions = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.sibling = null; + fiber.stateNode = null; + fiber.updateQueue = null; - if (__DEV__) { - fiber._debugOwner = null; + if (__DEV__) { + fiber._debugOwner = null; + } + } else { + // Clear cyclical Fiber fields. This level alone is designed to roughly + // approximate the planned Fiber refactor. In that world, `setState` will be + // bound to a special "instance" object instead of a Fiber. The Instance + // object will not have any of these fields. It will only be connected to + // the fiber tree via a single link at the root. So if this level alone is + // sufficient to fix memory issues, that bodes well for our plans. + fiber.child = null; + fiber.deletions = null; + fiber.sibling = null; + + // I'm intentionally not clearing the `return` field in this level. We + // already disconnect the `return` pointer at the root of the deleted + // subtree (in `detachFiberMutation`). Besides, `return` by itself is not + // cyclical — it's only cyclical when combined with `child`, `sibling`, and + // `alternate`. But we'll clear it in the next level anyway, just in case. + + if (__DEV__) { + fiber._debugOwner = null; + } + + if (deletedTreeCleanUpLevel >= 3) { + // Theoretically, nothing in here should be necessary, because we already + // disconnected the fiber from the tree. So even if something leaks this + // particular fiber, it won't leak anything else + // + // The purpose of this branch is to be super aggressive so we can measure + // if there's any difference in memory impact. If there is, that could + // indicate a React leak we don't know about. + + // For host components, disconnect host instance -> fiber pointer. + if (fiber.tag === HostComponent) { + const hostInstance: Instance = fiber.stateNode; + if (hostInstance !== null) { + detachDeletedInstance(hostInstance); + } + } + + fiber.return = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.stateNode = null; + // TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead. + fiber.updateQueue = null; + } } } @@ -1629,11 +1690,8 @@ function commitDeletion( renderPriorityLevel, ); } - const alternate = current.alternate; + detachFiberMutation(current); - if (alternate !== null) { - detachFiberMutation(alternate); - } } function commitWork(current: Fiber | null, finishedWork: Fiber): void { @@ -2308,14 +2366,34 @@ function commitPassiveUnmountEffects_begin() { fiberToDelete, fiber, ); + } - // Now that passive effects have been processed, it's safe to detach lingering pointers. - const alternate = fiberToDelete.alternate; - detachFiberAfterEffects(fiberToDelete); - if (alternate !== null) { - detachFiberAfterEffects(alternate); + if (deletedTreeCleanUpLevel >= 1) { + // A fiber was deleted from this parent fiber, but it's still part of + // the previous (alternate) parent fiber's list of children. Because + // children are a linked list, an earlier sibling that's still alive + // will be connected to the deleted fiber via its `alternate`: + // + // live fiber + // --alternate--> previous live fiber + // --sibling--> deleted fiber + // + // We can't disconnect `alternate` on nodes that haven't been deleted + // yet, but we can disconnect the `sibling` and `child` pointers. + const previousFiber = fiber.alternate; + if (previousFiber !== null) { + let detachedChild = previousFiber.child; + if (detachedChild !== null) { + previousFiber.child = null; + do { + const detachedSibling = detachedChild.sibling; + detachedChild.sibling = null; + detachedChild = detachedSibling; + } while (detachedChild !== null); + } } } + nextEffect = fiber; } } @@ -2392,7 +2470,8 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin( resetCurrentDebugFiberInDEV(); const child = fiber.child; - // TODO: Only traverse subtree if it has a PassiveStatic flag + // TODO: Only traverse subtree if it has a PassiveStatic flag. (But, if we + // do this, still need to handle `deletedTreeCleanUpLevel` correctly.) if (child !== null) { ensureCorrectReturnPointer(child, fiber); nextEffect = child; @@ -2409,19 +2488,35 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete( ) { while (nextEffect !== null) { const fiber = nextEffect; - if (fiber === deletedSubtreeRoot) { - nextEffect = null; - return; + const sibling = fiber.sibling; + const returnFiber = fiber.return; + + if (deletedTreeCleanUpLevel >= 2) { + // Recursively traverse the entire deleted tree and clean up fiber fields. + // This is more aggressive than ideal, and the long term goal is to only + // have to detach the deleted tree at the root. + detachFiberAfterEffects(fiber); + if (fiber === deletedSubtreeRoot) { + nextEffect = null; + return; + } + } else { + // This is the default branch (level 0). We do not recursively clear all + // the fiber fields. Only the root of the deleted subtree. + if (fiber === deletedSubtreeRoot) { + detachFiberAfterEffects(fiber); + nextEffect = null; + return; + } } - const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + ensureCorrectReturnPointer(sibling, returnFiber); nextEffect = sibling; return; } - nextEffect = fiber.return; + nextEffect = returnFiber; } } diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 5bfdf3a305f32..6a6e9c5e040c3 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -73,6 +73,7 @@ export const preparePortalMount = $$$hostConfig.preparePortalMount; export const prepareScopeUpdate = $$$hostConfig.preparePortalMount; export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope; export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority; +export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance; // ------------------- // Microtasks diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index a787d31840af3..bac900c469fa7 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -360,3 +360,7 @@ export function prepareScopeUpdate(scopeInstance: Object, inst: Object): void { export function getInstanceFromScope(scopeInstance: Object): null | Object { return nodeToInstanceMap.get(scopeInstance) || null; } + +export function detachDeletedInstance(node: Instance): void { + // noop +} diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 77079bba3a6a1..a1e9c42d3d1c8 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -112,6 +112,19 @@ export const disableNativeComponentFrames = false; // If there are no still-mounted boundaries, the errors should be rethrown. export const skipUnmountedBoundaries = false; +// When a node is unmounted, recurse into the Fiber subtree and clean out +// references. Each level cleans up more fiber fields than the previous level. +// As far as we know, React itself doesn't leak, but because the Fiber contains +// cycles, even a single leak in product code can cause us to retain large +// amounts of memory. +// +// The long term plan is to remove the cycles, but in the meantime, we clear +// additional fields to mitigate. +// +// It's an enum so that we can experiment with different levels of +// aggressiveness. +export const deletedTreeCleanUpLevel = 1; + // -------------------------- // Future APIs to be deprecated // -------------------------- diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 2e2f64e4bc5d0..7d5c5a6c40cc4 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -46,6 +46,7 @@ export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; export const disableNativeComponentFrames = false; export const skipUnmountedBoundaries = false; +export const deletedTreeCleanUpLevel = 1; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 34ee7d2b7a843..cd2979fc400f9 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; export const disableNativeComponentFrames = false; export const skipUnmountedBoundaries = false; +export const deletedTreeCleanUpLevel = 1; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index bd5e91885ca44..c7f1f1a4739d4 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; export const disableNativeComponentFrames = false; export const skipUnmountedBoundaries = false; +export const deletedTreeCleanUpLevel = 1; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 70c5dc4144ee8..895ebad1d809e 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 enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; export const disableNativeComponentFrames = false; export const skipUnmountedBoundaries = false; +export const deletedTreeCleanUpLevel = 1; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index df6bbdf03fbed..832f56e6fa093 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; export const disableNativeComponentFrames = false; export const skipUnmountedBoundaries = false; +export const deletedTreeCleanUpLevel = 1; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 42003be9a388b..351b0934c7924 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = false; export const disableNativeComponentFrames = false; export const skipUnmountedBoundaries = false; +export const deletedTreeCleanUpLevel = 1; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 5ab680976a9e4..b25ac0073e944 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -45,6 +45,7 @@ export const enableLegacyFBSupport = !__EXPERIMENTAL__; export const enableFilterEmptyStringAttributesDOM = false; export const disableNativeComponentFrames = false; export const skipUnmountedBoundaries = true; +export const deletedTreeCleanUpLevel = 1; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index e2496510ef24c..503d263fc9be6 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -52,6 +52,7 @@ export const disableNativeComponentFrames = false; export const createRootStrictEffectsByDefault = false; export const enableStrictEffects = false; export const enableUseRefAccessWarning = __VARIANT__; +export const deletedTreeCleanUpLevel = __VARIANT__ ? 3 : 1; export const enableProfilerNestedUpdateScheduledHook = __VARIANT__; export const disableSchedulerTimeoutInWorkLoop = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index c29856558751e..dab1a37bc5c69 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -32,6 +32,7 @@ export const { disableNativeComponentFrames, disableSchedulerTimeoutInWorkLoop, enableLazyContextPropagation, + deletedTreeCleanUpLevel, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build.