diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 0c879d6415f9..71eee4abd433 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<48b4420f0396dcad107bf821fcec61b3>> */ /** @@ -498,6 +498,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun skipActivityIdentityAssertionOnHostPause(): Boolean = accessor.skipActivityIdentityAssertionOnHostPause() + /** + * When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them. + */ + @JvmStatic + public fun skipUnchangedChildrenInDifferentiator(): Boolean = accessor.skipUnchangedChildrenInDifferentiator() + /** * Override getClipBounds on Android views to return the padding box when overflow is hidden */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 28329721fab8..112d87549783 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<71d0554dc45df7d5365f81b7ec8c3d5b>> + * @generated SignedSource<> */ /** @@ -98,6 +98,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null + private var skipUnchangedChildrenInDifferentiatorCache: Boolean? = null private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null @@ -817,6 +818,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun skipUnchangedChildrenInDifferentiator(): Boolean { + var cached = skipUnchangedChildrenInDifferentiatorCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.skipUnchangedChildrenInDifferentiator() + skipUnchangedChildrenInDifferentiatorCache = cached + } + return cached + } + override fun syncAndroidClipBoundsWithOverflow(): Boolean { var cached = syncAndroidClipBoundsWithOverflowCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 6f83f1cc3dbd..c60fb1d1ca75 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<312cec99bd429d22e25f4e82d7fc4ee7>> + * @generated SignedSource<> */ /** @@ -184,6 +184,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun skipActivityIdentityAssertionOnHostPause(): Boolean + @DoNotStrip @JvmStatic public external fun skipUnchangedChildrenInDifferentiator(): Boolean + @DoNotStrip @JvmStatic public external fun syncAndroidClipBoundsWithOverflow(): Boolean @DoNotStrip @JvmStatic public external fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index f4998cb8b416..4be939f92c45 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<669f16e131d4a5ddc026ffecc6893575>> + * @generated SignedSource<<3b2153df84f2c0193f32a5c5bf312700>> */ /** @@ -179,6 +179,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun skipActivityIdentityAssertionOnHostPause(): Boolean = false + override fun skipUnchangedChildrenInDifferentiator(): Boolean = false + override fun syncAndroidClipBoundsWithOverflow(): Boolean = false override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 536ee78905d7..127e1645ec97 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<2a0c2c71f3da4657d0f7b26548542c0d>> */ /** @@ -102,6 +102,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null + private var skipUnchangedChildrenInDifferentiatorCache: Boolean? = null private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null @@ -899,6 +900,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun skipUnchangedChildrenInDifferentiator(): Boolean { + var cached = skipUnchangedChildrenInDifferentiatorCache + if (cached == null) { + cached = currentProvider.skipUnchangedChildrenInDifferentiator() + accessedFeatureFlags.add("skipUnchangedChildrenInDifferentiator") + skipUnchangedChildrenInDifferentiatorCache = cached + } + return cached + } + override fun syncAndroidClipBoundsWithOverflow(): Boolean { var cached = syncAndroidClipBoundsWithOverflowCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 0143013e3ccd..2c85ff852523 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<18267103ef4f3eb22f23a15b808528c2>> + * @generated SignedSource<> */ /** @@ -179,6 +179,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun skipActivityIdentityAssertionOnHostPause(): Boolean + @DoNotStrip public fun skipUnchangedChildrenInDifferentiator(): Boolean + @DoNotStrip public fun syncAndroidClipBoundsWithOverflow(): Boolean @DoNotStrip public fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 526398c18c11..1fec3ed0299a 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<568d6e4175fb51e09bc3d4441ffa9741>> + * @generated SignedSource<> */ /** @@ -507,6 +507,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool skipUnchangedChildrenInDifferentiator() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("skipUnchangedChildrenInDifferentiator"); + return method(javaProvider_); + } + bool syncAndroidClipBoundsWithOverflow() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("syncAndroidClipBoundsWithOverflow"); @@ -997,6 +1003,11 @@ bool JReactNativeFeatureFlagsCxxInterop::skipActivityIdentityAssertionOnHostPaus return ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause(); } +bool JReactNativeFeatureFlagsCxxInterop::skipUnchangedChildrenInDifferentiator( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator(); +} + bool JReactNativeFeatureFlagsCxxInterop::syncAndroidClipBoundsWithOverflow( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow(); @@ -1342,6 +1353,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "skipActivityIdentityAssertionOnHostPause", JReactNativeFeatureFlagsCxxInterop::skipActivityIdentityAssertionOnHostPause), + makeNativeMethod( + "skipUnchangedChildrenInDifferentiator", + JReactNativeFeatureFlagsCxxInterop::skipUnchangedChildrenInDifferentiator), makeNativeMethod( "syncAndroidClipBoundsWithOverflow", JReactNativeFeatureFlagsCxxInterop::syncAndroidClipBoundsWithOverflow), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 5f3fdda386cd..01ff405d654c 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2e0f936c003aafc456ab3f960ca4b2f3>> + * @generated SignedSource<<79e3a5e32cd586b32e2c20c4f586c207>> */ /** @@ -264,6 +264,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool skipActivityIdentityAssertionOnHostPause( facebook::jni::alias_ref); + static bool skipUnchangedChildrenInDifferentiator( + facebook::jni::alias_ref); + static bool syncAndroidClipBoundsWithOverflow( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index adc3531518ef..7d054417d5c4 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -338,6 +338,10 @@ bool ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause() { return getAccessor().skipActivityIdentityAssertionOnHostPause(); } +bool ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator() { + return getAccessor().skipUnchangedChildrenInDifferentiator(); +} + bool ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow() { return getAccessor().syncAndroidClipBoundsWithOverflow(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index cd4d40533d5f..e7ee221223c8 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<62dbb584d6aaf595314636a81346e3e5>> + * @generated SignedSource<<349d1fffdceddf1b43927718ee193317>> */ /** @@ -431,6 +431,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool skipActivityIdentityAssertionOnHostPause(); + /** + * When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them. + */ + RN_EXPORT static bool skipUnchangedChildrenInDifferentiator(); + /** * Override getClipBounds on Android views to return the padding box when overflow is hidden */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 93ef1cc62ace..3196668902a1 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<3e4c90095377b1c9ec5280eeec3f93b5>> */ /** @@ -1433,6 +1433,24 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { + auto flagValue = skipUnchangedChildrenInDifferentiator_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(78, "skipUnchangedChildrenInDifferentiator"); + + flagValue = currentProvider_->skipUnchangedChildrenInDifferentiator(); + skipUnchangedChildrenInDifferentiator_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { auto flagValue = syncAndroidClipBoundsWithOverflow_.load(); @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(79, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(80, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(81, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(82, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(83, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "useFabricInterop"); + markFlagAsAccessed(84, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(85, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useNestedScrollViewAndroid"); + markFlagAsAccessed(86, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useSharedAnimatedBackend"); + markFlagAsAccessed(87, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(88, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useTurboModuleInterop"); + markFlagAsAccessed(89, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(90, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1658,7 +1676,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "viewCullingOutsetRatio"); + markFlagAsAccessed(91, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "viewTransitionEnabled"); + markFlagAsAccessed(92, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(93, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "virtualViewPrerenderRatio"); + markFlagAsAccessed(94, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 5c05c3bc5427..7e20b5ccca05 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<074e6ad1d773f289f93ce1d16656ec6e>> + * @generated SignedSource<<45f1e05d7349e95252b6d022e79f806c>> */ /** @@ -112,6 +112,7 @@ class ReactNativeFeatureFlagsAccessor { bool shouldPressibilityUseW3CPointerEventsForHover(); bool shouldTriggerResponderTransferOnScrollAndroid(); bool skipActivityIdentityAssertionOnHostPause(); + bool skipUnchangedChildrenInDifferentiator(); bool syncAndroidClipBoundsWithOverflow(); bool traceTurboModulePromiseRejectionsOnAndroid(); bool updateRuntimeShadowNodeReferencesOnCommit(); @@ -139,7 +140,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 94> accessedFeatureFlags_; + std::array, 95> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -219,6 +220,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> shouldPressibilityUseW3CPointerEventsForHover_; std::atomic> shouldTriggerResponderTransferOnScrollAndroid_; std::atomic> skipActivityIdentityAssertionOnHostPause_; + std::atomic> skipUnchangedChildrenInDifferentiator_; std::atomic> syncAndroidClipBoundsWithOverflow_; std::atomic> traceTurboModulePromiseRejectionsOnAndroid_; std::atomic> updateRuntimeShadowNodeReferencesOnCommit_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 82208e3861b9..eb80fb346539 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<52db1adfed5fd99e4ca145b50dc413a0>> + * @generated SignedSource<<05d7627f826b260b80cb5c849b9a3deb>> */ /** @@ -341,6 +341,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool skipUnchangedChildrenInDifferentiator() override { + return false; + } + bool syncAndroidClipBoundsWithOverflow() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 4d40ac107fc4..994f349e8339 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8d27c5f3feb4c8000922012882701b7e>> + * @generated SignedSource<> */ /** @@ -749,6 +749,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::skipActivityIdentityAssertionOnHostPause(); } + bool skipUnchangedChildrenInDifferentiator() override { + auto value = values_["skipUnchangedChildrenInDifferentiator"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::skipUnchangedChildrenInDifferentiator(); + } + bool syncAndroidClipBoundsWithOverflow() override { auto value = values_["syncAndroidClipBoundsWithOverflow"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 3ba8405c14ff..1554cfc8177e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7e5839223cfce81a42708f876c4c77ee>> + * @generated SignedSource<> */ /** @@ -105,6 +105,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool shouldPressibilityUseW3CPointerEventsForHover() = 0; virtual bool shouldTriggerResponderTransferOnScrollAndroid() = 0; virtual bool skipActivityIdentityAssertionOnHostPause() = 0; + virtual bool skipUnchangedChildrenInDifferentiator() = 0; virtual bool syncAndroidClipBoundsWithOverflow() = 0; virtual bool traceTurboModulePromiseRejectionsOnAndroid() = 0; virtual bool updateRuntimeShadowNodeReferencesOnCommit() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index f2d4210f8d23..b477518cbd29 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<04ac6206f8cbad557091ae4543103465>> */ /** @@ -434,6 +434,11 @@ bool NativeReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause( return ReactNativeFeatureFlags::skipActivityIdentityAssertionOnHostPause(); } +bool NativeReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator(); +} + bool NativeReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 4aafb51f00fc..957e7f50311f 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<84b944e162fc53799326c584a5edd17d>> + * @generated SignedSource<<3356b4ddc2ac35a521698944d3dd5d49>> */ /** @@ -194,6 +194,8 @@ class NativeReactNativeFeatureFlags bool skipActivityIdentityAssertionOnHostPause(jsi::Runtime& runtime); + bool skipUnchangedChildrenInDifferentiator(jsi::Runtime& runtime); + bool syncAndroidClipBoundsWithOverflow(jsi::Runtime& runtime); bool traceTurboModulePromiseRejectionsOnAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp index 41de087d3f80..2e47428d3f58 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include "internal/CullingContext.h" @@ -91,6 +92,35 @@ sliceChildShadowNodeViewPairsFromViewNodePair( cullingContext); } +/* + * Slices the children of a matched pair in both trees. With + * `skipUnchangedChildrenInDifferentiator`, children that are the same node in + * both trees are left out when possible: they produce no mutations. + */ +static void sliceMatchedPairChildren( + const ShadowViewNodePair& oldPair, + const ShadowViewNodePair& newPair, + ViewNodePairScope& scope, + const CullingContext& oldCullingContext, + const CullingContext& newCullingContext, + std::vector& oldChildPairs, + std::vector& newChildPairs) { + if (ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator() && + sliceChangedChildShadowNodeViewPairs( + oldPair, + newPair, + scope, + false /* allowFlattened */, + oldChildPairs, + newChildPairs)) { + return; + } + oldChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( + oldPair, scope, false /* allowFlattened */, oldCullingContext); + newChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( + newPair, scope, false /* allowFlattened */, newCullingContext); +} + /* * Before we start to diff, let's make sure all our core data structures are * in good shape to deliver the best performance. @@ -299,10 +329,16 @@ static void updateMatchedPairSubtrees( if (oldPair.shadowNode != newPair.shadowNode || oldCullingContextCopy != newCullingContextCopy) { ViewNodePairScope innerScope{}; - auto oldGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( - oldPair, innerScope, false, oldCullingContextCopy); - auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( - newPair, innerScope, false, newCullingContextCopy); + std::vector oldGrandChildPairs; + std::vector newGrandChildPairs; + sliceMatchedPairChildren( + oldPair, + newPair, + innerScope, + oldCullingContextCopy, + newCullingContextCopy, + oldGrandChildPairs, + newGrandChildPairs); const size_t newGrandChildPairsSize = newGrandChildPairs.size(); calculateShadowViewMutations( @@ -948,10 +984,16 @@ static void calculateShadowViewMutations( (oldChildPair.shadowNode != newChildPair.shadowNode || adjustedOldCullingContext != adjustedNewCullingContext)) { ViewNodePairScope innerScope{}; - auto oldGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( - oldChildPair, innerScope, false, adjustedOldCullingContext); - auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair( - newChildPair, innerScope, false, adjustedNewCullingContext); + std::vector oldGrandChildPairs; + std::vector newGrandChildPairs; + sliceMatchedPairChildren( + oldChildPair, + newChildPair, + innerScope, + adjustedOldCullingContext, + adjustedNewCullingContext, + oldGrandChildPairs, + newGrandChildPairs); const size_t newGrandChildPairsSize = newGrandChildPairs.size(); @@ -1382,18 +1424,16 @@ ShadowViewMutation::List calculateShadowViewMutations( oldRootShadowView, newRootShadowView, {})); } - auto sliceOne = sliceChildShadowNodeViewPairs( + std::vector sliceOne; + std::vector sliceTwo; + sliceMatchedPairChildren( ShadowViewNodePair{.shadowNode = &oldRootShadowNode}, - viewNodePairScope, - false /* allowFlattened */, - {} /* layoutOffset */, - {} /* cullingContext */); - auto sliceTwo = sliceChildShadowNodeViewPairs( ShadowViewNodePair{.shadowNode = &newRootShadowNode}, viewNodePairScope, - false /* allowFlattened */, - {} /* layoutOffset */, - {} /* cullingContext */); + {} /* oldCullingContext */, + {} /* newCullingContext */, + sliceOne, + sliceTwo); mutations.reserve(256); diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp index 644d4e4d88a4..4fe57dd5a31a 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp @@ -47,6 +47,95 @@ static void reorderInPlaceIfNeeded( pairs.begin(), pairs.end(), &shouldFirstPairComesBeforeSecondOne); } +/* + * Whether the slicer leaves out `shadowNode` and its descendants. + */ +static bool isHiddenWhenSlicing(const ShadowNode& shadowNode) { + // T153547836: Disabled on Android because the mounting infrastructure + // is not fully ready yet. + return +#ifdef ANDROID + ReactNativeFeatureFlags::useTraitHiddenOnAndroid() && +#endif + shadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden); +} + +/* + * A child of `parentShadowNode` as the slicer represents it: its view, moved + * by the offset of the flattened views it was hoisted out of, the origin its + * own flattened children are offset by, and whether it forms a view and + * flattens its children. + */ +struct ChildSlice { + ShadowView shadowView; + Point origin; + bool isConcreteView; + bool areChildrenFlattened; +}; + +static ChildSlice sliceChild( + const ShadowNode& parentShadowNode, + const ShadowNode& childShadowNode, + ShadowView shadowView, + Point layoutOffset) { + auto origin = layoutOffset; + if (shadowView.layoutMetrics != EmptyLayoutMetrics) { + origin += shadowView.layoutMetrics.frame.origin; + shadowView.layoutMetrics.frame.origin += layoutOffset; + } + + // This might not be a FormsView, or a FormsStackingContext. We let the + // differ handle removal of flattened views from the Mounting layer and + // shuffling their children around. + bool childrenFormStackingContexts = parentShadowNode.getTraits().check( + ShadowNodeTraits::Trait::ChildrenFormStackingContext); + bool isConcreteView = + (childShadowNode.getTraits().check(ShadowNodeTraits::Trait::FormsView) || + childrenFormStackingContexts) && + !childShadowNode.getTraits().check( + ShadowNodeTraits::Trait::ForceFlattenView); + bool areChildrenFlattened = + (!childShadowNode.getTraits().check( + ShadowNodeTraits::Trait::FormsStackingContext) && + !childrenFormStackingContexts) || + childShadowNode.getTraits().check( + ShadowNodeTraits::Trait::ForceFlattenView); + + return { + .shadowView = std::move(shadowView), + .origin = origin, + .isConcreteView = isConcreteView, + .areChildrenFlattened = areChildrenFlattened}; +} + +/* + * Adds `childSlice` to `scope` and its pair to `pairList`: statically + * positioned pairs go before absolutely positioned ones. + */ +static void addChildSlice( + std::vector& pairList, + size_t& startOfStaticIndex, + ViewNodePairScope& scope, + const ChildSlice& childSlice, + const ShadowNode& childShadowNode) { + scope.push_back( + {childSlice.shadowView, + &childShadowNode, + childSlice.areChildrenFlattened, + childSlice.isConcreteView, + childSlice.areChildrenFlattened ? childSlice.origin : Point{}}); + + if (childSlice.shadowView.layoutMetrics.positionType == + PositionType::Static) { + auto it = pairList.begin(); + std::advance(it, startOfStaticIndex); + pairList.insert(it, &scope.back()); + startOfStaticIndex++; + } else { + pairList.push_back(&scope.back()); + } +} + static void sliceChildShadowNodeViewPairsRecursively( std::vector& pairList, size_t& startOfStaticIndex, @@ -56,13 +145,7 @@ static void sliceChildShadowNodeViewPairsRecursively( const CullingContext& cullingContext) { for (const auto& sharedChildShadowNode : shadowNode.getChildren()) { auto& childShadowNode = *sharedChildShadowNode; - // T153547836: Disabled on Android because the mounting infrastructure - // is not fully ready yet. - if ( -#ifdef ANDROID - ReactNativeFeatureFlags::useTraitHiddenOnAndroid() && -#endif - childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) { + if (isHiddenWhenSlicing(childShadowNode)) { continue; } auto shadowView = ShadowView(childShadowNode); @@ -97,71 +180,208 @@ static void sliceChildShadowNodeViewPairsRecursively( } } - auto origin = layoutOffset; auto cullingContextCopy = cullingContext.adjustCullingContextIfNeeded( {.shadowView = shadowView, .shadowNode = &childShadowNode}); - if (shadowView.layoutMetrics != EmptyLayoutMetrics) { - origin += shadowView.layoutMetrics.frame.origin; - shadowView.layoutMetrics.frame.origin += layoutOffset; + auto childSlice = sliceChild( + shadowNode, childShadowNode, std::move(shadowView), layoutOffset); + addChildSlice( + pairList, startOfStaticIndex, scope, childSlice, childShadowNode); + + if (!childSlice.areChildrenFlattened) { + continue; + } + if (childSlice.shadowView.layoutMetrics.positionType == + PositionType::Static) { + sliceChildShadowNodeViewPairsRecursively( + pairList, + startOfStaticIndex, + scope, + childSlice.origin, + childShadowNode, + cullingContextCopy); + } else { + size_t pairListSize = pairList.size(); + sliceChildShadowNodeViewPairsRecursively( + pairList, + pairListSize, + scope, + childSlice.origin, + childShadowNode, + cullingContextCopy); } + } +} - // This might not be a FormsView, or a FormsStackingContext. We let the - // differ handle removal of flattened views from the Mounting layer and - // shuffling their children around. - bool childrenFormStackingContexts = shadowNode.getTraits().check( - ShadowNodeTraits::Trait::ChildrenFormStackingContext); - bool isConcreteView = (childShadowNode.getTraits().check( - ShadowNodeTraits::Trait::FormsView) || - childrenFormStackingContexts) && - !childShadowNode.getTraits().check( - ShadowNodeTraits::Trait::ForceFlattenView); - bool areChildrenFlattened = - (!childShadowNode.getTraits().check( - ShadowNodeTraits::Trait::FormsStackingContext) && - !childrenFormStackingContexts) || - childShadowNode.getTraits().check( - ShadowNodeTraits::Trait::ForceFlattenView); - - Point storedOrigin = {}; - if (areChildrenFlattened) { - storedOrigin = origin; +/* + * Sets the mount index of each pair after reordering. + */ +static void assignMountIndexes(std::vector& pairList) { + size_t mountIndex = 0; + for (auto child : pairList) { + child->mountIndex = + (child->isConcreteView ? mountIndex++ : static_cast(-1)); + } +} + +static bool sliceChangedChildShadowNodeViewPairsRecursively( + std::vector& oldPairList, + size_t& oldStartOfStaticIndex, + std::vector& newPairList, + size_t& newStartOfStaticIndex, + ViewNodePairScope& scope, + Point oldLayoutOffset, + Point newLayoutOffset, + const ShadowNode& oldShadowNode, + const ShadowNode& newShadowNode) { + const auto& oldChildren = oldShadowNode.getChildren(); + const auto& newChildren = newShadowNode.getChildren(); + if (oldChildren.size() != newChildren.size() || + oldShadowNode.getTraits().check( + ShadowNodeTraits::Trait::ChildrenFormStackingContext) != + newShadowNode.getTraits().check( + ShadowNodeTraits::Trait::ChildrenFormStackingContext)) { + return false; + } + + bool isSameLayoutOffset = oldLayoutOffset == newLayoutOffset; + for (size_t i = 0; i < oldChildren.size(); i++) { + const auto& oldChildShadowNode = *oldChildren[i]; + const auto& newChildShadowNode = *newChildren[i]; + + bool isHidden = isHiddenWhenSlicing(oldChildShadowNode); + if (isHidden != isHiddenWhenSlicing(newChildShadowNode)) { + return false; + } + if (isHidden) { + continue; + } + + // The same node at the same offset gives the same pairs in both lists, + // which the differ matches without a mutation. + if (&oldChildShadowNode == &newChildShadowNode && isSameLayoutOffset) { + continue; + } + + if (oldChildShadowNode.getTag() != newChildShadowNode.getTag() || + oldChildShadowNode.getOrderIndex() != + newChildShadowNode.getOrderIndex()) { + return false; + } + + auto oldChildSlice = sliceChild( + oldShadowNode, + oldChildShadowNode, + ShadowView(oldChildShadowNode), + oldLayoutOffset); + auto newChildSlice = sliceChild( + newShadowNode, + newChildShadowNode, + ShadowView(newChildShadowNode), + newLayoutOffset); + bool isStatic = newChildSlice.shadowView.layoutMetrics.positionType == + PositionType::Static; + if (oldChildSlice.isConcreteView != newChildSlice.isConcreteView || + oldChildSlice.areChildrenFlattened != + newChildSlice.areChildrenFlattened || + (oldChildSlice.shadowView.layoutMetrics.positionType == + PositionType::Static) != isStatic) { + return false; + } + + addChildSlice( + oldPairList, + oldStartOfStaticIndex, + scope, + oldChildSlice, + oldChildShadowNode); + addChildSlice( + newPairList, + newStartOfStaticIndex, + scope, + newChildSlice, + newChildShadowNode); + + if (!newChildSlice.areChildrenFlattened) { + continue; } - scope.push_back( - {shadowView, - &childShadowNode, - areChildrenFlattened, - isConcreteView, - storedOrigin}); - - if (shadowView.layoutMetrics.positionType == PositionType::Static) { - auto it = pairList.begin(); - std::advance(it, startOfStaticIndex); - pairList.insert(it, &scope.back()); - startOfStaticIndex++; - if (areChildrenFlattened) { - sliceChildShadowNodeViewPairsRecursively( - pairList, - startOfStaticIndex, - scope, - origin, - childShadowNode, - cullingContextCopy); + if (isStatic) { + if (!sliceChangedChildShadowNodeViewPairsRecursively( + oldPairList, + oldStartOfStaticIndex, + newPairList, + newStartOfStaticIndex, + scope, + oldChildSlice.origin, + newChildSlice.origin, + oldChildShadowNode, + newChildShadowNode)) { + return false; } } else { - pairList.push_back(&scope.back()); - if (areChildrenFlattened) { - size_t pairListSize = pairList.size(); - sliceChildShadowNodeViewPairsRecursively( - pairList, - pairListSize, - scope, - origin, - childShadowNode, - cullingContextCopy); + size_t oldPairListSize = oldPairList.size(); + size_t newPairListSize = newPairList.size(); + if (!sliceChangedChildShadowNodeViewPairsRecursively( + oldPairList, + oldPairListSize, + newPairList, + newPairListSize, + scope, + oldChildSlice.origin, + newChildSlice.origin, + oldChildShadowNode, + newChildShadowNode)) { + return false; } } } + return true; +} + +bool sliceChangedChildShadowNodeViewPairs( + const ShadowViewNodePair& oldShadowNodePair, + const ShadowViewNodePair& newShadowNodePair, + ViewNodePairScope& scope, + bool allowFlattened, + std::vector& oldPairList, + std::vector& newPairList) { + if (ReactNativeFeatureFlags::enableViewCulling()) { + return false; + } + + oldPairList.clear(); + newPairList.clear(); + bool isOldPairSkipped = oldShadowNodePair.flattened && + oldShadowNodePair.isConcreteView && !allowFlattened; + bool isNewPairSkipped = newShadowNodePair.flattened && + newShadowNodePair.isConcreteView && !allowFlattened; + if (isOldPairSkipped != isNewPairSkipped) { + return false; + } + if (isOldPairSkipped) { + return true; + } + + size_t oldStartOfStaticIndex = 0; + size_t newStartOfStaticIndex = 0; + if (!sliceChangedChildShadowNodeViewPairsRecursively( + oldPairList, + oldStartOfStaticIndex, + newPairList, + newStartOfStaticIndex, + scope, + oldShadowNodePair.contextOrigin, + newShadowNodePair.contextOrigin, + *oldShadowNodePair.shadowNode, + *newShadowNodePair.shadowNode)) { + return false; + } + + reorderInPlaceIfNeeded(oldPairList); + reorderInPlaceIfNeeded(newPairList); + assignMountIndexes(oldPairList); + assignMountIndexes(newPairList); + return true; } std::vector sliceChildShadowNodeViewPairs( @@ -191,12 +411,7 @@ std::vector sliceChildShadowNodeViewPairs( // Sorting pairs based on `orderIndex` if needed. reorderInPlaceIfNeeded(pairList); - // Set list and mountIndex for each after reordering - size_t mountIndex = 0; - for (auto child : pairList) { - child->mountIndex = - (child->isConcreteView ? mountIndex++ : static_cast(-1)); - } + assignMountIndexes(pairList); return pairList; } diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.h b/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.h index ffbdb168a573..428a36d7bdb9 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.h @@ -52,4 +52,26 @@ std::vector sliceChildShadowNodeViewPairs( Point layoutOffset, const CullingContext &cullingContext); +/* + * Slices the children of two revisions of a node side by side, like two calls + * to `sliceChildShadowNodeViewPairs`, but leaves out each child that is the + * same `ShadowNode` in both revisions at the same offset. Such a child and its + * flattened descendants give equal pairs in both lists, which the differ + * matches without a mutation, so the mutations computed from the shorter lists + * are the same. + * + * Returns false if the revisions differ in a way that can change the order or + * the kind of the pairs (child count, tags, flattening, position type, order + * index, visibility or `ChildrenFormStackingContext`), or if view culling is + * enabled. The lists are then unspecified and the caller has to slice both + * revisions in full. + */ +bool sliceChangedChildShadowNodeViewPairs( + const ShadowViewNodePair &oldShadowNodePair, + const ShadowViewNodePair &newShadowNodePair, + ViewNodePairScope &viewNodePairScope, + bool allowFlattened, + std::vector &oldPairList, + std::vector &newPairList); + } // namespace facebook::react diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 2c0786728b99..c4e2de3f908e 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -889,6 +889,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + skipUnchangedChildrenInDifferentiator: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-20', + description: + 'When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, syncAndroidClipBoundsWithOverflow: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index c190a2488b08..13f1c066058d 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<5d5b555169e7a8ecb94003145039b4d3>> * @flow strict * @noformat */ @@ -127,6 +127,7 @@ export type ReactNativeFeatureFlags = Readonly<{ shouldPressibilityUseW3CPointerEventsForHover: Getter, shouldTriggerResponderTransferOnScrollAndroid: Getter, skipActivityIdentityAssertionOnHostPause: Getter, + skipUnchangedChildrenInDifferentiator: Getter, syncAndroidClipBoundsWithOverflow: Getter, traceTurboModulePromiseRejectionsOnAndroid: Getter, updateRuntimeShadowNodeReferencesOnCommit: Getter, @@ -531,6 +532,10 @@ export const shouldTriggerResponderTransferOnScrollAndroid: Getter = cr * Skip activity identity assertion in ReactHostImpl::onHostPause() */ export const skipActivityIdentityAssertionOnHostPause: Getter = createNativeFlagGetter('skipActivityIdentityAssertionOnHostPause', false); +/** + * When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them. + */ +export const skipUnchangedChildrenInDifferentiator: Getter = createNativeFlagGetter('skipUnchangedChildrenInDifferentiator', false); /** * Override getClipBounds on Android views to return the padding box when overflow is hidden */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 673dc0ed3bf8..69f131dab539 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<40cc7b87d7658fb5aa3d33f468eff3fc>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -103,6 +103,7 @@ export interface Spec extends TurboModule { readonly shouldPressibilityUseW3CPointerEventsForHover?: () => boolean; readonly shouldTriggerResponderTransferOnScrollAndroid?: () => boolean; readonly skipActivityIdentityAssertionOnHostPause?: () => boolean; + readonly skipUnchangedChildrenInDifferentiator?: () => boolean; readonly syncAndroidClipBoundsWithOverflow?: () => boolean; readonly traceTurboModulePromiseRejectionsOnAndroid?: () => boolean; readonly updateRuntimeShadowNodeReferencesOnCommit?: () => boolean; diff --git a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js index c260930ad584..5f7af7cb14fd 100644 --- a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js +++ b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @fantom_flags enableFabricCommitBranching:* + * @fantom_flags skipUnchangedChildrenInDifferentiator:* * @flow strict-local * @format */ @@ -16,7 +17,7 @@ import type {HostInstance} from 'react-native'; import * as Fantom from '@react-native/fantom'; import nullthrows from 'nullthrows'; import * as React from 'react'; -import {View} from 'react-native'; +import {ScrollView, View} from 'react-native'; describe('ViewFlattening', () => { /** @@ -515,6 +516,133 @@ describe('ViewFlattening', () => { }); }); +describe('unchanged children', () => { + // Rendered once and never again, so its shadow node is the same in every + // later revision of the tree. + const Unchanged = React.memo(function Unchanged(props: {nativeID: string}) { + return ; + }); + + test('only the changed child of a node is updated', () => { + const root = Fantom.createRoot(); + const render = (width: number) => + Fantom.runTask(() => { + root.render( + + + + + , + ); + }); + + render(10); + root.takeMountingManagerLogs(); + + render(20); + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "View", nativeID: "changed"}', + ]); + }); + + test('an unchanged child of a flattened view that moves is updated', () => { + const root = Fantom.createRoot(); + const render = (marginTop: number) => + Fantom.runTask(() => { + root.render( + + + + + , + ); + }); + + render(0); + root.takeMountingManagerLogs(); + + // The parent grows by the margin, and A moves with the flattened view it + // is hoisted out of, so it is updated although its node is unchanged. + render(20); + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "View", nativeID: "parent"}', + 'Update {type: "View", nativeID: "A"}', + ]); + }); + + test('an unchanged child of a scroll view is not updated when it scrolls', () => { + const root = Fantom.createRoot({viewportWidth: 100, viewportHeight: 100}); + const scrollViewRef = React.createRef(); + + Fantom.runTask(() => { + root.render( + + + + + + + , + ); + }); + root.takeMountingManagerLogs(); + + Fantom.scrollTo(nullthrows(scrollViewRef.current), {x: 0, y: 20}); + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "ScrollView", nativeID: "scrollView"}', + ]); + }); + + test('a child added next to unchanged children is inserted', () => { + const root = Fantom.createRoot(); + const render = (showAdded: boolean) => + Fantom.runTask(() => { + root.render( + + + {showAdded ? : null} + + , + ); + }); + + render(false); + root.takeMountingManagerLogs(); + + render(true); + expect(root.takeMountingManagerLogs()).toEqual([ + 'Create {type: "View", nativeID: "added"}', + 'Insert {type: "View", parentNativeID: "parent", index: 1, nativeID: "added"}', + ]); + }); + + test('a child whose zIndex changes next to unchanged children is reordered', () => { + const root = Fantom.createRoot(); + const render = (zIndex: number) => + Fantom.runTask(() => { + root.render( + + + + + , + ); + }); + + render(0); + root.takeMountingManagerLogs(); + + render(1); + expect(root.takeMountingManagerLogs()).toEqual([ + 'Update {type: "View", nativeID: "changed"}', + 'Remove {type: "View", parentNativeID: "parent", index: 2, nativeID: "B"}', + 'Remove {type: "View", parentNativeID: "parent", index: 1, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "parent", index: 0, nativeID: "A"}', + 'Insert {type: "View", parentNativeID: "parent", index: 1, nativeID: "B"}', + ]); + }); +}); + describe('reconciliation of setNativeProps and React commit', () => { it('props set by setNativeProps must not be overriden by React commit', () => { const root = Fantom.createRoot();