diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js
index 10930caf2f98..a0bbcb74c69b 100644
--- a/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js
+++ b/packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js
@@ -4,6 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
+ * @fantom_flags useLISAlgorithmInDifferentiator:*
* @flow strict-local
* @format
*/
@@ -173,4 +174,98 @@ Fantom.unstable_benchmark
root.destroy();
},
}),
+ )
+ .test.each(
+ [10, 50, 100],
+ n => `reorder ${n.toString()} children (move first to last)`,
+ () => {
+ Fantom.runTask(() => root.render(testViews));
+ },
+ n => {
+ let original: React.MixedElement;
+ let reordered: React.MixedElement;
+ return {
+ beforeAll: () => {
+ const children = [];
+ for (let i = 0; i < n; i++) {
+ children.push(
+ ,
+ );
+ }
+ original = (
+
+ {children}
+
+ );
+ // Move first child to last
+ const reorderedChildren = [...children.slice(1), children[0]];
+ reordered = (
+
+ {reorderedChildren}
+
+ );
+ },
+ beforeEach: () => {
+ root = Fantom.createRoot();
+ Fantom.runTask(() => root.render(original));
+ // $FlowExpectedError[incompatible-type]
+ testViews = reordered;
+ },
+ afterEach: () => {
+ root.destroy();
+ },
+ };
+ },
+ )
+ .test.each(
+ [10, 50, 100],
+ n => `reorder ${n.toString()} children (swap first two)`,
+ () => {
+ Fantom.runTask(() => root.render(testViews));
+ },
+ n => {
+ let original: React.MixedElement;
+ let reordered: React.MixedElement;
+ return {
+ beforeAll: () => {
+ const children = [];
+ for (let i = 0; i < n; i++) {
+ children.push(
+ ,
+ );
+ }
+ original = (
+
+ {children}
+
+ );
+ // Swap first two children — both algorithms handle this equally
+ const swapped = [children[1], children[0], ...children.slice(2)];
+ reordered = (
+
+ {swapped}
+
+ );
+ },
+ beforeEach: () => {
+ root = Fantom.createRoot();
+ Fantom.runTask(() => root.render(original));
+ // $FlowExpectedError[incompatible-type]
+ testViews = reordered;
+ },
+ afterEach: () => {
+ root.destroy();
+ },
+ };
+ },
);
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 7be4cb067bba..d49efd1aadc7 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<<5144fb0350b71394206d614c68ef87f0>>
+ * @generated SignedSource<<61964fd9ddf11ed5c2848da3f4d0b490>>
*/
/**
@@ -510,6 +510,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun useFabricInterop(): Boolean = accessor.useFabricInterop()
+ /**
+ * Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation.
+ */
+ @JvmStatic
+ public fun useLISAlgorithmInDifferentiator(): Boolean = accessor.useLISAlgorithmInDifferentiator()
+
/**
* When enabled, the native view configs are used in bridgeless mode.
*/
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 a77a2ff90fe9..fc7fc079ed5f 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<>
+ * @generated SignedSource<<4ce2605ff71e60b6096a211ff902e994>>
*/
/**
@@ -100,6 +100,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null
private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null
private var useFabricInteropCache: Boolean? = null
+ private var useLISAlgorithmInDifferentiatorCache: Boolean? = null
private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null
private var useNestedScrollViewAndroidCache: Boolean? = null
private var useSharedAnimatedBackendCache: Boolean? = null
@@ -831,6 +832,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}
+ override fun useLISAlgorithmInDifferentiator(): Boolean {
+ var cached = useLISAlgorithmInDifferentiatorCache
+ if (cached == null) {
+ cached = ReactNativeFeatureFlagsCxxInterop.useLISAlgorithmInDifferentiator()
+ useLISAlgorithmInDifferentiatorCache = cached
+ }
+ return cached
+ }
+
override fun useNativeViewConfigsInBridgelessMode(): Boolean {
var cached = useNativeViewConfigsInBridgelessModeCache
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 fa8758c0901b..e3ffd419a8e8 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<>
+ * @generated SignedSource<<5f3573c9983cb54c5df527a3053ebbae>>
*/
/**
@@ -188,6 +188,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
@DoNotStrip @JvmStatic public external fun useFabricInterop(): Boolean
+ @DoNotStrip @JvmStatic public external fun useLISAlgorithmInDifferentiator(): Boolean
+
@DoNotStrip @JvmStatic public external fun useNativeViewConfigsInBridgelessMode(): Boolean
@DoNotStrip @JvmStatic public external fun useNestedScrollViewAndroid(): 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 e467f8cd7327..e8791ff06299 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<>
+ * @generated SignedSource<<67d638f79b7b06a087f63563c2e5ff95>>
*/
/**
@@ -183,6 +183,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
override fun useFabricInterop(): Boolean = true
+ override fun useLISAlgorithmInDifferentiator(): Boolean = false
+
override fun useNativeViewConfigsInBridgelessMode(): Boolean = false
override fun useNestedScrollViewAndroid(): 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 99d211c64a30..6ae836432104 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<<7b87f5541ecf881d8ce51c5edd5b99b0>>
+ * @generated SignedSource<>
*/
/**
@@ -104,6 +104,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null
private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null
private var useFabricInteropCache: Boolean? = null
+ private var useLISAlgorithmInDifferentiatorCache: Boolean? = null
private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null
private var useNestedScrollViewAndroidCache: Boolean? = null
private var useSharedAnimatedBackendCache: Boolean? = null
@@ -915,6 +916,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}
+ override fun useLISAlgorithmInDifferentiator(): Boolean {
+ var cached = useLISAlgorithmInDifferentiatorCache
+ if (cached == null) {
+ cached = currentProvider.useLISAlgorithmInDifferentiator()
+ accessedFeatureFlags.add("useLISAlgorithmInDifferentiator")
+ useLISAlgorithmInDifferentiatorCache = cached
+ }
+ return cached
+ }
+
override fun useNativeViewConfigsInBridgelessMode(): Boolean {
var cached = useNativeViewConfigsInBridgelessModeCache
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 de1d05f86ef3..a952c252dbdb 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<>
+ * @generated SignedSource<<540700c0c0f3259a093a98ad639478ba>>
*/
/**
@@ -183,6 +183,8 @@ public interface ReactNativeFeatureFlagsProvider {
@DoNotStrip public fun useFabricInterop(): Boolean
+ @DoNotStrip public fun useLISAlgorithmInDifferentiator(): Boolean
+
@DoNotStrip public fun useNativeViewConfigsInBridgelessMode(): Boolean
@DoNotStrip public fun useNestedScrollViewAndroid(): 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 fe397f8b1e41..0bedbceeee25 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<>
+ * @generated SignedSource<<6c088ccf18868fc6e54d83c7483b6607>>
*/
/**
@@ -519,6 +519,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}
+ bool useLISAlgorithmInDifferentiator() override {
+ static const auto method =
+ getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useLISAlgorithmInDifferentiator");
+ return method(javaProvider_);
+ }
+
bool useNativeViewConfigsInBridgelessMode() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useNativeViewConfigsInBridgelessMode");
@@ -983,6 +989,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useFabricInterop(
return ReactNativeFeatureFlags::useFabricInterop();
}
+bool JReactNativeFeatureFlagsCxxInterop::useLISAlgorithmInDifferentiator(
+ facebook::jni::alias_ref /*unused*/) {
+ return ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator();
+}
+
bool JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode(
facebook::jni::alias_ref /*unused*/) {
return ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode();
@@ -1304,6 +1315,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"useFabricInterop",
JReactNativeFeatureFlagsCxxInterop::useFabricInterop),
+ makeNativeMethod(
+ "useLISAlgorithmInDifferentiator",
+ JReactNativeFeatureFlagsCxxInterop::useLISAlgorithmInDifferentiator),
makeNativeMethod(
"useNativeViewConfigsInBridgelessMode",
JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode),
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 08276eab5edd..e4b4e467fdd7 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<<5433b4a2f4a0574591a38017422edac8>>
+ * @generated SignedSource<<73cfe749b34b786e25b683c499889e48>>
*/
/**
@@ -270,6 +270,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool useFabricInterop(
facebook::jni::alias_ref);
+ static bool useLISAlgorithmInDifferentiator(
+ facebook::jni::alias_ref);
+
static bool useNativeViewConfigsInBridgelessMode(
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 4f058038abbd..2a0bc39e528d 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<>
*/
/**
@@ -346,6 +346,10 @@ bool ReactNativeFeatureFlags::useFabricInterop() {
return getAccessor().useFabricInterop();
}
+bool ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator() {
+ return getAccessor().useLISAlgorithmInDifferentiator();
+}
+
bool ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode() {
return getAccessor().useNativeViewConfigsInBridgelessMode();
}
diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h
index 7185d625c251..9b24d7eec3b1 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<<4811a81c7839f2be5c8a127e6c8e310b>>
+ * @generated SignedSource<<6c175f21aaa8d084c7b0be0625d5d77d>>
*/
/**
@@ -439,6 +439,11 @@ class ReactNativeFeatureFlags {
*/
RN_EXPORT static bool useFabricInterop();
+ /**
+ * Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation.
+ */
+ RN_EXPORT static bool useLISAlgorithmInDifferentiator();
+
/**
* When enabled, the native view configs are used in bridgeless mode.
*/
diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp
index 936e3c590d50..3b529d3f83bf 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<>
*/
/**
@@ -1469,6 +1469,24 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() {
return flagValue.value();
}
+bool ReactNativeFeatureFlagsAccessor::useLISAlgorithmInDifferentiator() {
+ auto flagValue = useLISAlgorithmInDifferentiator_.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(80, "useLISAlgorithmInDifferentiator");
+
+ flagValue = currentProvider_->useLISAlgorithmInDifferentiator();
+ useLISAlgorithmInDifferentiator_ = flagValue;
+ }
+
+ return flagValue.value();
+}
+
bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() {
auto flagValue = useNativeViewConfigsInBridgelessMode_.load();
@@ -1478,7 +1496,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(80, "useNativeViewConfigsInBridgelessMode");
+ markFlagAsAccessed(81, "useNativeViewConfigsInBridgelessMode");
flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode();
useNativeViewConfigsInBridgelessMode_ = flagValue;
@@ -1496,7 +1514,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(81, "useNestedScrollViewAndroid");
+ markFlagAsAccessed(82, "useNestedScrollViewAndroid");
flagValue = currentProvider_->useNestedScrollViewAndroid();
useNestedScrollViewAndroid_ = flagValue;
@@ -1514,7 +1532,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(82, "useSharedAnimatedBackend");
+ markFlagAsAccessed(83, "useSharedAnimatedBackend");
flagValue = currentProvider_->useSharedAnimatedBackend();
useSharedAnimatedBackend_ = flagValue;
@@ -1532,7 +1550,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(83, "useTraitHiddenOnAndroid");
+ markFlagAsAccessed(84, "useTraitHiddenOnAndroid");
flagValue = currentProvider_->useTraitHiddenOnAndroid();
useTraitHiddenOnAndroid_ = flagValue;
@@ -1550,7 +1568,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(84, "useTurboModuleInterop");
+ markFlagAsAccessed(85, "useTurboModuleInterop");
flagValue = currentProvider_->useTurboModuleInterop();
useTurboModuleInterop_ = flagValue;
@@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
- markFlagAsAccessed(85, "useTurboModules");
+ markFlagAsAccessed(86, "useTurboModules");
flagValue = currentProvider_->useTurboModules();
useTurboModules_ = flagValue;
@@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useUnorderedMapInDifferentiator() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
- markFlagAsAccessed(86, "useUnorderedMapInDifferentiator");
+ markFlagAsAccessed(87, "useUnorderedMapInDifferentiator");
flagValue = currentProvider_->useUnorderedMapInDifferentiator();
useUnorderedMapInDifferentiator_ = flagValue;
@@ -1604,7 +1622,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(87, "viewCullingOutsetRatio");
+ markFlagAsAccessed(88, "viewCullingOutsetRatio");
flagValue = currentProvider_->viewCullingOutsetRatio();
viewCullingOutsetRatio_ = flagValue;
@@ -1622,7 +1640,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(88, "viewTransitionEnabled");
+ markFlagAsAccessed(89, "viewTransitionEnabled");
flagValue = currentProvider_->viewTransitionEnabled();
viewTransitionEnabled_ = flagValue;
@@ -1640,7 +1658,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(89, "virtualViewPrerenderRatio");
+ markFlagAsAccessed(90, "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 e76f1322f58d..b572c9d7b944 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<<06dba77b9b76d06d5c338ed8a97e33f5>>
+ * @generated SignedSource<<29e7a9ef1807aaecaf5440e024a5a2f2>>
*/
/**
@@ -112,6 +112,7 @@ class ReactNativeFeatureFlagsAccessor {
bool updateRuntimeShadowNodeReferencesOnCommitThread();
bool useAlwaysAvailableJSErrorHandling();
bool useFabricInterop();
+ bool useLISAlgorithmInDifferentiator();
bool useNativeViewConfigsInBridgelessMode();
bool useNestedScrollViewAndroid();
bool useSharedAnimatedBackend();
@@ -133,7 +134,7 @@ class ReactNativeFeatureFlagsAccessor {
std::unique_ptr currentProvider_;
bool wasOverridden_;
- std::array, 90> accessedFeatureFlags_;
+ std::array, 91> accessedFeatureFlags_;
std::atomic> commonTestFlag_;
std::atomic> cdpInteractionMetricsEnabled_;
@@ -215,6 +216,7 @@ class ReactNativeFeatureFlagsAccessor {
std::atomic> updateRuntimeShadowNodeReferencesOnCommitThread_;
std::atomic> useAlwaysAvailableJSErrorHandling_;
std::atomic> useFabricInterop_;
+ std::atomic> useLISAlgorithmInDifferentiator_;
std::atomic> useNativeViewConfigsInBridgelessMode_;
std::atomic> useNestedScrollViewAndroid_;
std::atomic> useSharedAnimatedBackend_;
diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h
index 1a6289888d15..c64f1748335b 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<<0934d867533630904fc69e30e7a929b3>>
+ * @generated SignedSource<<2cf1c7be6b0086da159550454273ce2d>>
*/
/**
@@ -347,6 +347,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
return true;
}
+ bool useLISAlgorithmInDifferentiator() override {
+ return false;
+ }
+
bool useNativeViewConfigsInBridgelessMode() override {
return false;
}
diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h
index 5c939775920a..141d4c5f2dbc 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<<25d1f9cb509dbd8274e3a00237d2ea62>>
+ * @generated SignedSource<<20a808bb8708d8088f3d7aae8d58b6b2>>
*/
/**
@@ -765,6 +765,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef
return ReactNativeFeatureFlagsDefaults::useFabricInterop();
}
+ bool useLISAlgorithmInDifferentiator() override {
+ auto value = values_["useLISAlgorithmInDifferentiator"];
+ if (!value.isNull()) {
+ return value.getBool();
+ }
+
+ return ReactNativeFeatureFlagsDefaults::useLISAlgorithmInDifferentiator();
+ }
+
bool useNativeViewConfigsInBridgelessMode() override {
auto value = values_["useNativeViewConfigsInBridgelessMode"];
if (!value.isNull()) {
diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h
index cc3fbc19b88d..42c8c97942d5 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<>
+ * @generated SignedSource<<8c52d3da48fbc27e8e23493f81a93d55>>
*/
/**
@@ -105,6 +105,7 @@ class ReactNativeFeatureFlagsProvider {
virtual bool updateRuntimeShadowNodeReferencesOnCommitThread() = 0;
virtual bool useAlwaysAvailableJSErrorHandling() = 0;
virtual bool useFabricInterop() = 0;
+ virtual bool useLISAlgorithmInDifferentiator() = 0;
virtual bool useNativeViewConfigsInBridgelessMode() = 0;
virtual bool useNestedScrollViewAndroid() = 0;
virtual bool useSharedAnimatedBackend() = 0;
diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp
index 1fbd4198e5d2..54f33abe3339 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<<0b3534a570416860aa1ffc7e1d808090>>
+ * @generated SignedSource<<51f57b07e238ca9c13f4c7ec363eb9a0>>
*/
/**
@@ -444,6 +444,11 @@ bool NativeReactNativeFeatureFlags::useFabricInterop(
return ReactNativeFeatureFlags::useFabricInterop();
}
+bool NativeReactNativeFeatureFlags::useLISAlgorithmInDifferentiator(
+ jsi::Runtime& /*runtime*/) {
+ return ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator();
+}
+
bool NativeReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode();
diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h
index 081db38ca2fd..18ab3a19fd2f 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<<0aeea7c4fa2a8aa4180c83bbd0746250>>
+ * @generated SignedSource<<8368d761c6a95fcdbf804681a2bf65d3>>
*/
/**
@@ -196,6 +196,8 @@ class NativeReactNativeFeatureFlags
bool useFabricInterop(jsi::Runtime& runtime);
+ bool useLISAlgorithmInDifferentiator(jsi::Runtime& runtime);
+
bool useNativeViewConfigsInBridgelessMode(jsi::Runtime& runtime);
bool useNestedScrollViewAndroid(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 89f27bf71f9a..10b6e24a12c1 100644
--- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
@@ -12,6 +12,7 @@
#include
#include "internal/CullingContext.h"
#include "internal/DiffMap.h"
+#include "internal/LongestIncreasingSubsequence.h"
#include "internal/ShadowViewNodePair.h"
#include "internal/sliceChildShadowNodeViewPairs.h"
@@ -1048,13 +1049,275 @@ static void calculateShadowViewMutations(
oldCullingContext,
newCullingContextCopy);
}
+ } else if (ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator()) {
+ // LIS-based Stage 4: find the Longest Increasing Subsequence of
+ // new-list positions among old children to minimize REMOVE/INSERT
+ // mutations. Items in the LIS maintain their relative order and
+ // don't need REMOVE+INSERT — only items outside the LIS are moved.
+ auto remainingOldCount = oldChildPairs.size() - lastIndexAfterFirstStage;
+ auto remainingNewCount = newChildPairs.size() - lastIndexAfterFirstStage;
+
+ // Build newRemainingPairs (required by updateMatchedPairSubtrees for
+ // flattening logic) and tag→index map for O(1) lookups in Step 1.
+ auto newRemainingPairs =
+ DiffMap(remainingNewCount);
+ auto newTagToIndex = DiffMap(remainingNewCount);
+ for (size_t i = lastIndexAfterFirstStage; i < newChildPairs.size(); i++) {
+ auto& newChildPair = *newChildPairs[i];
+ newRemainingPairs.insert({newChildPair.shadowView.tag, &newChildPair});
+ newTagToIndex.insert({newChildPair.shadowView.tag, i});
+ }
+
+ // Step 1: Map old children to their positions in the new list.
+ std::vector oldToNewPos(remainingOldCount);
+ std::vector oldExistsInNew(remainingOldCount, false);
+
+ for (size_t i = 0; i < remainingOldCount; i++) {
+ auto oldIdx = lastIndexAfterFirstStage + i;
+ Tag oldTag = oldChildPairs[oldIdx]->shadowView.tag;
+ auto it = newTagToIndex.find(oldTag);
+ if (it != newTagToIndex.end()) {
+ oldToNewPos[i] = it->second;
+ oldExistsInNew[i] = true;
+ }
+ }
+
+ // Step 2: Compute LIS of new-list positions.
+ auto inLIS = longestIncreasingSubsequence(oldToNewPos, oldExistsInNew);
+
+ auto deletionCandidatePairs = std::vector{};
+ deletionCandidatePairs.reserve(remainingOldCount);
+
+ // New-child-indexed flag: true = LIS match that stays in place.
+ auto isLISMatch = std::vector(remainingNewCount, false);
+
+ // Step 4: Process old children.
+ // CRITICAL: check newRemainingPairs at runtime (not the pre-computed
+ // oldExistsInNew). The flattener erases entries from newRemainingPairs
+ // as it consumes them during flatten/unflatten — using the pre-computed
+ // snapshot would cause double-processing of flattened nodes.
+ react_native_assert(inLIS.size() == remainingOldCount);
+ for (size_t i = 0; i < remainingOldCount; i++) {
+ auto oldIdx = lastIndexAfterFirstStage + i;
+ auto& oldChildPair = *oldChildPairs[oldIdx];
+ Tag oldTag = oldChildPair.shadowView.tag;
+
+ auto newIt = newRemainingPairs.find(oldTag);
+ if (newIt == newRemainingPairs.end()) {
+ // Not in new list or consumed by flattening -> REMOVE.
+ if (!oldChildPair.isConcreteView) {
+ continue;
+ }
+
+ DEBUG_LOGS({
+ LOG(ERROR) << "Differ LIS Branch: Removing deleted tag: "
+ << oldChildPair << " with parent: [" << parentTag << "]";
+ });
+
+ // Edge case: complex (un)flattening — node exists in other tree.
+ if (oldChildPair.inOtherTree() &&
+ oldChildPair.otherTreePair->isConcreteView) {
+ const ShadowView& otherTreeView =
+ oldChildPair.otherTreePair->shadowView;
+ mutationContainer.removeMutations.push_back(
+ ShadowViewMutation::RemoveMutation(
+ parentTag,
+ otherTreeView,
+ static_cast(oldChildPair.mountIndex)));
+ continue;
+ }
+
+ mutationContainer.removeMutations.push_back(
+ ShadowViewMutation::RemoveMutation(
+ parentTag,
+ oldChildPair.shadowView,
+ static_cast(oldChildPair.mountIndex)));
+ deletionCandidatePairs.push_back(&oldChildPair);
+
+ } else if (inLIS[i]) {
+ // In LIS -> stays in place, just UPDATE + subtree recursion.
+ auto& newChildPair = *newIt->second;
+
+ DEBUG_LOGS({
+ LOG(ERROR) << "Differ LIS Branch: Matched in-order (LIS) at old "
+ << oldIdx << ": " << oldChildPair << " with parent: ["
+ << parentTag << "]";
+ });
+
+ // For LIS matches with concrete-ness changes, we must use
+ // (true, false) to avoid generating INSERT in updateMatchedPair.
+ // INSERT mutations must be in new-child order (Step 5), not
+ // old-child order (Step 4). Generating INSERT here would put it
+ // out of order relative to Step 5's INSERTs.
+ bool concreteChanged =
+ oldChildPair.isConcreteView != newChildPair.isConcreteView;
+
+ updateMatchedPair(
+ mutationContainer,
+ true,
+ !concreteChanged,
+ parentTag,
+ oldChildPair,
+ newChildPair);
+
+ updateMatchedPairSubtrees(
+ scope,
+ mutationContainer,
+ newRemainingPairs,
+ oldChildPairs,
+ parentTag,
+ oldChildPair,
+ newChildPair,
+ oldCullingContext,
+ newCullingContext);
+
+ if (!concreteChanged) {
+ // Check if this LIS match was consumed by flattening.
+ // updateMatchedPairSubtrees may erase entries from
+ // newRemainingPairs during flatten/unflatten transitions.
+ // If consumed, the node was reparented elsewhere and needs
+ // REMOVE from this parent.
+ if (newRemainingPairs.find(oldTag) != newRemainingPairs.end()) {
+ isLISMatch[oldToNewPos[i] - lastIndexAfterFirstStage] = true;
+ } else if (oldChildPair.isConcreteView) {
+ if (oldChildPair.inOtherTree() &&
+ oldChildPair.otherTreePair->isConcreteView) {
+ mutationContainer.removeMutations.push_back(
+ ShadowViewMutation::RemoveMutation(
+ parentTag,
+ oldChildPair.otherTreePair->shadowView,
+ static_cast(oldChildPair.mountIndex)));
+ } else {
+ mutationContainer.removeMutations.push_back(
+ ShadowViewMutation::RemoveMutation(
+ parentTag,
+ oldChildPair.shadowView,
+ static_cast(oldChildPair.mountIndex)));
+ deletionCandidatePairs.push_back(&oldChildPair);
+ }
+ }
+ }
+ // concreteChanged: not in isLISMatch, Step 5 handles INSERT.
+
+ } else {
+ // In new list but NOT in LIS -> REMOVE from old position.
+ // Will be re-inserted at new position in Step 5.
+ auto& newChildPair = *newIt->second;
+
+ DEBUG_LOGS({
+ LOG(ERROR)
+ << "Differ LIS Branch: Matched out-of-order (not in LIS) at old "
+ << oldIdx << ": " << oldChildPair << " with parent: ["
+ << parentTag << "]";
+ });
+
+ updateMatchedPair(
+ mutationContainer,
+ true,
+ false,
+ parentTag,
+ oldChildPair,
+ newChildPair);
+
+ updateMatchedPairSubtrees(
+ scope,
+ mutationContainer,
+ newRemainingPairs,
+ oldChildPairs,
+ parentTag,
+ oldChildPair,
+ newChildPair,
+ oldCullingContext,
+ newCullingContext);
+ }
+ }
+
+ // Step 5: Process new children — INSERT + CREATE.
+ // Generate INSERT for every non-LIS-matched concrete new child.
+ // Only generate CREATE for genuinely new children (not in other tree
+ // from flattening).
+ for (size_t i = lastIndexAfterFirstStage; i < newChildPairs.size(); i++) {
+ auto& newChildPair = *newChildPairs[i];
+
+ if (!newChildPair.isConcreteView) {
+ continue;
+ }
+
+ // LIS matches stay in place — no INSERT needed.
+ if (isLISMatch[i - lastIndexAfterFirstStage]) {
+ continue;
+ }
+
+ DEBUG_LOGS({
+ LOG(ERROR) << "Differ LIS Branch: Inserting tag: " << newChildPair
+ << " with parent: [" << parentTag << "]"
+ << (newChildPair.inOtherTree() ? " (in other tree)" : "");
+ });
+
+ mutationContainer.insertMutations.push_back(
+ ShadowViewMutation::InsertMutation(
+ parentTag,
+ newChildPair.shadowView,
+ static_cast(newChildPair.mountIndex)));
+
+ // Only CREATE genuinely new children (not matched by flattening).
+ if (!newChildPair.inOtherTree()) {
+ mutationContainer.createMutations.push_back(
+ ShadowViewMutation::CreateMutation(newChildPair.shadowView));
+
+ auto newCullingContextCopy =
+ newCullingContext.adjustCullingContextIfNeeded(newChildPair);
+
+ ViewNodePairScope innerScope{};
+ calculateShadowViewMutations(
+ innerScope,
+ mutationContainer.downwardMutations,
+ newChildPair.shadowView.tag,
+ {},
+ sliceChildShadowNodeViewPairsFromViewNodePair(
+ newChildPair, innerScope, false, newCullingContextCopy),
+ oldCullingContext,
+ newCullingContextCopy);
+ }
+ }
+
+ // Step 6: Generate DELETE for deletion candidates.
+ for (const auto* deletionCandidatePtr : deletionCandidatePairs) {
+ const auto& oldChildPair = *deletionCandidatePtr;
+
+ DEBUG_LOGS({
+ LOG(ERROR) << "Differ LIS Branch: Deleting removed tag: "
+ << oldChildPair << " with parent: [" << parentTag << "]";
+ });
+
+ if (!oldChildPair.inOtherTree() && oldChildPair.isConcreteView) {
+ mutationContainer.deleteMutations.push_back(
+ ShadowViewMutation::DeleteMutation(oldChildPair.shadowView));
+ auto oldCullingContextCopy =
+ oldCullingContext.adjustCullingContextIfNeeded(oldChildPair);
+
+ ViewNodePairScope innerScope{};
+ auto grandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
+ oldChildPair, innerScope, false, oldCullingContextCopy);
+ calculateShadowViewMutations(
+ innerScope,
+ mutationContainer.destructiveDownwardMutations,
+ oldChildPair.shadowView.tag,
+ std::move(grandChildPairs),
+ {},
+ oldCullingContextCopy,
+ newCullingContext);
+ }
+ }
} else {
+ // Existing greedy Stage 4 algorithm.
// Collect map of tags in the new list
- auto remainingCount = newChildPairs.size() - index;
+ auto remainingCount = newChildPairs.size() - lastIndexAfterFirstStage;
auto newRemainingPairs = DiffMap(remainingCount);
auto newInsertedPairs = DiffMap(remainingCount);
auto deletionCandidatePairs = DiffMap{};
- for (; index < newChildPairs.size(); index++) {
+ for (index = lastIndexAfterFirstStage; index < newChildPairs.size();
+ index++) {
auto& newChildPair = *newChildPairs[index];
newRemainingPairs.insert({newChildPair.shadowView.tag, &newChildPair});
}
diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/internal/LongestIncreasingSubsequence.h b/packages/react-native/ReactCommon/react/renderer/mounting/internal/LongestIncreasingSubsequence.h
new file mode 100644
index 000000000000..1f31ead210cf
--- /dev/null
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/internal/LongestIncreasingSubsequence.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace facebook::react {
+
+/**
+ * Computes the Longest Increasing Subsequence (LIS) of the given
+ * sequence of values using O(n log n) patience sorting.
+ *
+ * Returns a vector of the same size as `values`, where
+ * result[i] == true means values[i] is part of the LIS.
+ *
+ * Only elements where include[i] == true are considered;
+ * elements with include[i] == false are ignored and will always
+ * be false in the result.
+ */
+inline std::vector longestIncreasingSubsequence(
+ const std::vector &values,
+ const std::vector &include)
+{
+ react_native_assert(values.size() == include.size());
+
+ size_t n = values.size();
+ std::vector inLIS(n, false);
+
+ if (n == 0) {
+ return inLIS;
+ }
+
+ // Collect indices of included elements.
+ std::vector indices;
+ indices.reserve(n);
+ for (size_t i = 0; i < n; i++) {
+ if (include[i]) {
+ indices.push_back(i);
+ }
+ }
+
+ if (indices.empty()) {
+ return inLIS;
+ }
+
+ // tails[i] = smallest tail value of all increasing subsequences
+ // of length i+1.
+ std::vector tails;
+ // tailIndices[i] = index into `indices` whose value is tails[i].
+ std::vector tailIndices;
+ // predecessor[k] = index into `indices` of the predecessor of
+ // indices[k] in the LIS, or -1 if none.
+ std::vector predecessor(indices.size(), -1);
+
+ tails.reserve(indices.size());
+ tailIndices.reserve(indices.size());
+
+ for (size_t k = 0; k < indices.size(); k++) {
+ size_t val = values[indices[k]];
+
+ // Binary search for the first element in tails >= val.
+ auto it = std::lower_bound(tails.begin(), tails.end(), val);
+ auto pos = static_cast(it - tails.begin());
+
+ if (it == tails.end()) {
+ tails.push_back(val);
+ tailIndices.push_back(k);
+ } else {
+ *it = val;
+ tailIndices[pos] = k;
+ }
+
+ if (pos > 0) {
+ predecessor[k] = static_cast(tailIndices[pos - 1]);
+ }
+ }
+
+ // Reconstruct LIS by tracing predecessors from the last element.
+ int current = static_cast(tailIndices.back());
+ while (current >= 0) {
+ inLIS[indices[static_cast(current)]] = true;
+ current = predecessor[static_cast(current)];
+ }
+
+ return inLIS;
+}
+
+} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/tests/LongestIncreasingSubsequenceTest.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/tests/LongestIncreasingSubsequenceTest.cpp
new file mode 100644
index 000000000000..428543348f7e
--- /dev/null
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/tests/LongestIncreasingSubsequenceTest.cpp
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+#include
+
+#include "internal/LongestIncreasingSubsequence.h"
+
+namespace facebook::react {
+
+TEST(LongestIncreasingSubsequenceTest, emptyInput) {
+ auto result = longestIncreasingSubsequence({}, {});
+ EXPECT_TRUE(result.empty());
+}
+
+TEST(LongestIncreasingSubsequenceTest, singleElement) {
+ auto result = longestIncreasingSubsequence({5}, {true});
+ ASSERT_EQ(result.size(), 1u);
+ EXPECT_TRUE(result[0]);
+}
+
+TEST(LongestIncreasingSubsequenceTest, alreadySorted) {
+ // [0, 1, 2, 3, 4] — entire sequence is the LIS.
+ std::vector values = {0, 1, 2, 3, 4};
+ std::vector include = {true, true, true, true, true};
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 5u);
+ for (size_t i = 0; i < 5; i++) {
+ EXPECT_TRUE(result[i]) << "index " << i << " should be in LIS";
+ }
+}
+
+TEST(LongestIncreasingSubsequenceTest, reverseSorted) {
+ // [4, 3, 2, 1, 0] — LIS length is 1.
+ std::vector values = {4, 3, 2, 1, 0};
+ std::vector include = {true, true, true, true, true};
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 5u);
+ int count = 0;
+ for (size_t i = 0; i < 5; i++) {
+ if (result[i]) {
+ count++;
+ }
+ }
+ EXPECT_EQ(count, 1);
+}
+
+TEST(LongestIncreasingSubsequenceTest, moveLastToFront) {
+ // Old: [A, B, C, D, E] mapped to new positions [1, 2, 3, 4, 0]
+ // LIS should be [1, 2, 3, 4] (indices 0-3), leaving index 4 out.
+ std::vector values = {1, 2, 3, 4, 0};
+ std::vector include = {true, true, true, true, true};
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 5u);
+ EXPECT_TRUE(result[0]); // value 1
+ EXPECT_TRUE(result[1]); // value 2
+ EXPECT_TRUE(result[2]); // value 3
+ EXPECT_TRUE(result[3]); // value 4
+ EXPECT_FALSE(result[4]); // value 0
+}
+
+TEST(LongestIncreasingSubsequenceTest, moveFirstToLast) {
+ // Old: [A, B, C, D, E] mapped to new positions [4, 0, 1, 2, 3]
+ // LIS should be [0, 1, 2, 3] (indices 1-4), leaving index 0 out.
+ std::vector values = {4, 0, 1, 2, 3};
+ std::vector include = {true, true, true, true, true};
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 5u);
+ EXPECT_FALSE(result[0]); // value 4
+ EXPECT_TRUE(result[1]); // value 0
+ EXPECT_TRUE(result[2]); // value 1
+ EXPECT_TRUE(result[3]); // value 2
+ EXPECT_TRUE(result[4]); // value 3
+}
+
+TEST(LongestIncreasingSubsequenceTest, withExcludedElements) {
+ // values = [1, _, 3, _, 0] where _ are excluded (deleted from new list)
+ std::vector values = {1, 999, 3, 999, 0};
+ std::vector include = {true, false, true, false, true};
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 5u);
+ EXPECT_FALSE(result[1]); // excluded
+ EXPECT_FALSE(result[3]); // excluded
+
+ // Among included: [1, 3, 0]. LIS is [1, 3] (length 2).
+ EXPECT_TRUE(result[0]); // value 1
+ EXPECT_TRUE(result[2]); // value 3
+ EXPECT_FALSE(result[4]); // value 0
+}
+
+TEST(LongestIncreasingSubsequenceTest, allExcluded) {
+ std::vector values = {3, 1, 2};
+ std::vector include = {false, false, false};
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 3u);
+ EXPECT_FALSE(result[0]);
+ EXPECT_FALSE(result[1]);
+ EXPECT_FALSE(result[2]);
+}
+
+TEST(LongestIncreasingSubsequenceTest, interleaved) {
+ // [3, 1, 4, 1, 5, 9, 2, 6]
+ // One valid LIS: [1, 4, 5, 9] or [1, 2, 6] etc. Length should be 4.
+ std::vector values = {3, 1, 4, 1, 5, 9, 2, 6};
+ std::vector include(8, true);
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 8u);
+ int count = 0;
+ size_t prev = 0;
+ bool first = true;
+ for (size_t i = 0; i < 8; i++) {
+ if (result[i]) {
+ count++;
+ if (!first) {
+ EXPECT_GT(values[i], prev) << "LIS must be strictly increasing";
+ }
+ prev = values[i];
+ first = false;
+ }
+ }
+ EXPECT_EQ(count, 4);
+}
+
+TEST(LongestIncreasingSubsequenceTest, swapTwoElements) {
+ // Old: [A, B, C, D] → New: [A, C, B, D] → positions [0, 2, 1, 3]
+ // LIS: [0, 1, 3] or [0, 2, 3] — length 3, one element out.
+ std::vector values = {0, 2, 1, 3};
+ std::vector include = {true, true, true, true};
+ auto result = longestIncreasingSubsequence(values, include);
+
+ ASSERT_EQ(result.size(), 4u);
+ int count = 0;
+ for (size_t i = 0; i < 4; i++) {
+ if (result[i]) {
+ count++;
+ }
+ }
+ EXPECT_EQ(count, 3);
+ // First and last should always be in LIS.
+ EXPECT_TRUE(result[0]); // value 0
+ EXPECT_TRUE(result[3]); // value 3
+}
+
+} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp
index fb22e2372cc8..c7b501b91c25 100644
--- a/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/tests/ShadowTreeLifeCycleTest.cpp
@@ -13,9 +13,14 @@
#include
#include
#include
+#include
+#include
+#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -346,7 +351,35 @@ static void testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
using namespace facebook::react;
-TEST(
+namespace {
+
+class LISFeatureFlagsOverride
+ : public facebook::react::ReactNativeFeatureFlagsDefaults {
+ public:
+ bool useLISAlgorithmInDifferentiator() override {
+ return true;
+ }
+};
+
+} // namespace
+
+// Parametrized lifecycle tests: each test runs with both the greedy
+// algorithm (false) and the LIS algorithm (true).
+class ShadowTreeLifecycleTest : public ::testing::TestWithParam {
+ protected:
+ void SetUp() override {
+ if (GetParam()) {
+ facebook::react::ReactNativeFeatureFlags::override(
+ std::make_unique());
+ }
+ }
+
+ void TearDown() override {
+ facebook::react::ReactNativeFeatureFlags::dangerouslyReset();
+ }
+};
+
+TEST_P(
ShadowTreeLifecycleTest,
stableBiggerTreeFewerIterationsOptimizedMovesFlattener) {
testShadowNodeTreeLifeCycle(
@@ -356,7 +389,7 @@ TEST(
/* stages */ 32);
}
-TEST(
+TEST_P(
ShadowTreeLifecycleTest,
stableBiggerTreeFewerIterationsOptimizedMovesFlattener2) {
testShadowNodeTreeLifeCycle(
@@ -366,7 +399,7 @@ TEST(
/* stages */ 32);
}
-TEST(
+TEST_P(
ShadowTreeLifecycleTest,
stableSmallerTreeMoreIterationsOptimizedMovesFlattener) {
testShadowNodeTreeLifeCycle(
@@ -376,7 +409,7 @@ TEST(
/* stages */ 32);
}
-TEST(
+TEST_P(
ShadowTreeLifecycleTest,
unstableSmallerTreeFewerIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
@@ -386,7 +419,7 @@ TEST(
/* stages */ 32);
}
-TEST(
+TEST_P(
ShadowTreeLifecycleTest,
unstableBiggerTreeFewerIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
@@ -396,7 +429,7 @@ TEST(
/* stages */ 32);
}
-TEST(
+TEST_P(
ShadowTreeLifecycleTest,
unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening) {
testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
@@ -407,20 +440,18 @@ TEST(
}
// failing test case found 4-25-2021
-// TODO: T213669056
-// TEST(
-// ShadowTreeLifecycleTest,
-// unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening_1167342011)
-// {
-// testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
-// /* seed */ 1167342011,
-// /* size */ 32,
-// /* repeats */ 512,
-// /* stages */ 32);
-// }
+TEST_P(
+ ShadowTreeLifecycleTest,
+ unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflattening_1167342011) {
+ testShadowNodeTreeLifeCycleExtensiveFlatteningUnflattening(
+ /* seed */ 1167342011,
+ /* size */ 32,
+ /* repeats */ 512,
+ /* stages */ 32);
+}
// You may uncomment this - locally only! - to generate failing seeds.
-// TEST(
+// TEST_P(
// ShadowTreeLifecycleTest,
// unstableSmallerTreeMoreIterationsExtensiveFlatteningUnflatteningManyRandom)
// {
@@ -435,3 +466,268 @@ TEST(
// /* stages */ 32);
// }
// }
+
+// Demonstrates that the LIS algorithm produces fewer mutations than the
+// greedy algorithm for a simple child reorder: [A,B,C,D,E] → [B,C,D,E,A].
+// The greedy two-pointer walk encounters A vs B mismatch and generates
+// excessive REMOVE+INSERT pairs. The LIS algorithm identifies that B,C,D,E
+// are already in increasing order and only moves A.
+TEST_P(ShadowTreeLifecycleTest, moveFirstChildToLast) {
+ auto builder = simpleComponentBuilder();
+
+ auto makeProps = [](const std::string& id) {
+ auto props = std::make_shared();
+ props->nativeId = id;
+ return props;
+ };
+
+ // clang-format off
+ auto rootElement =
+ Element()
+ .tag(1)
+ .children({
+ Element().tag(2).props(makeProps("A")),
+ Element().tag(3).props(makeProps("B")),
+ Element().tag(4).props(makeProps("C")),
+ Element().tag(5).props(makeProps("D")),
+ Element().tag(6).props(makeProps("E")),
+ });
+ // clang-format on
+
+ auto rootNode = builder.build(rootElement);
+
+ // Clone root with children reordered to [B, C, D, E, A].
+ auto children = rootNode->getChildren();
+ auto reorderedChildren =
+ std::make_shared>>();
+ for (size_t i = 1; i < children.size(); i++) {
+ reorderedChildren->push_back(children[i]);
+ }
+ reorderedChildren->push_back(children[0]);
+
+ auto reorderedRootNode = std::static_pointer_cast(
+ rootNode->ShadowNode::clone(
+ ShadowNodeFragment{
+ .props = ShadowNodeFragment::propsPlaceholder(),
+ .children = reorderedChildren}));
+
+ auto expected =
+ buildStubViewTreeWithoutUsingDifferentiator(*reorderedRootNode);
+ auto mutations = calculateShadowViewMutations(*rootNode, *reorderedRootNode);
+
+ auto isMoveOp = [](const ShadowViewMutation& m) {
+ return m.type == ShadowViewMutation::Remove ||
+ m.type == ShadowViewMutation::Insert;
+ };
+
+ if (ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator()) {
+ // LIS: 1 REMOVE + 1 INSERT = 2 move ops.
+ EXPECT_EQ(std::count_if(mutations.begin(), mutations.end(), isMoveOp), 2);
+ } else {
+ // Greedy: 4 REMOVE + 4 INSERT = 8 move ops.
+ EXPECT_EQ(std::count_if(mutations.begin(), mutations.end(), isMoveOp), 8);
+ }
+
+ auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode);
+ viewTree.mutate(mutations);
+ EXPECT_EQ(viewTree, expected);
+}
+
+// Exercises the LIS path where concreteChanged=true AND flattening
+// consumption occur simultaneously. Z(tag=7) is reordered to break head/tail
+// matching, forcing A, W, and C into the LIS path. W(tag=3) loses testId
+// (concrete→non-concrete, concreteChanged=true) while remaining in the LIS.
+// updateMatchedPairSubtrees triggers the flattener which promotes B(tag=6).
+// The if(!concreteChanged) block that normally checks consumption is bypassed,
+// so this test verifies the combination still produces correct mutations.
+TEST_P(ShadowTreeLifecycleTest, concreteChangedWithFlattening) {
+ auto builder = simpleComponentBuilder();
+
+ auto concreteProps = [](const std::string& id) {
+ auto props = std::make_shared();
+ props->testId = id;
+ return props;
+ };
+
+ // clang-format off
+ // Old: Root -> [Z, A, W(concrete, child B), C]
+ // Old flattened: [Z(7), A(2), W(3), C(4)]
+ auto rootElement =
+ Element()
+ .tag(1)
+ .children({
+ Element().tag(7).props(concreteProps("Z")),
+ Element().tag(2).props(concreteProps("A")),
+ Element().tag(3).props(concreteProps("W"))
+ .children({
+ Element().tag(6).props(concreteProps("B")),
+ }),
+ Element().tag(4).props(concreteProps("C")),
+ });
+ // clang-format on
+
+ auto rootNode = builder.build(rootElement);
+ auto children = rootNode->getChildren();
+ // children: [Z(7), A(2), W(3), C(4)]
+
+ // W loses testId → non-concrete (flattened). B(tag=6) promoted.
+ auto flatW = children[2]->clone(
+ ShadowNodeFragment{.props = std::make_shared()});
+
+ // New: Root -> [A, W(flattened), C, Z] — Z moved to end.
+ // New flattened: [A(2), W(3,non-concrete), B(6), C(4), Z(7)]
+ // Z's move breaks head matching; A, W, C enter LIS path.
+ // Among old remaining [Z,A,W,C] mapped to new positions,
+ // LIS includes A,W,C (increasing order), W has concreteChanged=true.
+ auto newChildren =
+ std::make_shared>>();
+ newChildren->push_back(children[1]); // A
+ newChildren->push_back(flatW); // W (flattened)
+ newChildren->push_back(children[3]); // C
+ newChildren->push_back(children[0]); // Z (moved to end)
+
+ auto newRootNode = std::static_pointer_cast(
+ rootNode->ShadowNode::clone(
+ ShadowNodeFragment{
+ .props = ShadowNodeFragment::propsPlaceholder(),
+ .children = newChildren}));
+
+ auto expected = buildStubViewTreeWithoutUsingDifferentiator(*newRootNode);
+ auto mutations = calculateShadowViewMutations(*rootNode, *newRootNode);
+ auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode);
+ viewTree.mutate(mutations);
+ EXPECT_EQ(viewTree, expected);
+}
+
+// Reverse: W(tag=3) gains testId (non-concrete→concrete, concreteChanged=true)
+// while in the LIS path. B(tag=6) was promoted by flattening and now gets
+// absorbed back into W via unflattening. Z reorder breaks head matching.
+TEST_P(ShadowTreeLifecycleTest, concreteChangedWithUnflatteningInLIS) {
+ auto builder = simpleComponentBuilder();
+
+ auto concreteProps = [](const std::string& id) {
+ auto props = std::make_shared();
+ props->testId = id;
+ return props;
+ };
+
+ // clang-format off
+ // Old: Root -> [Z, A, W(NON-concrete, child B(concrete)), C]
+ // Old flattened: [Z(7), A(2), W(3,non-concrete), B(6), C(4)]
+ auto rootElement =
+ Element()
+ .tag(1)
+ .children({
+ Element().tag(7).props(concreteProps("Z")),
+ Element().tag(2).props(concreteProps("A")),
+ Element().tag(3)
+ .children({
+ Element().tag(6).props(concreteProps("B")),
+ }),
+ Element().tag(4).props(concreteProps("C")),
+ });
+ // clang-format on
+
+ auto rootNode = builder.build(rootElement);
+ auto children = rootNode->getChildren();
+
+ // W gains testId → concrete (unflattened). B absorbed back into W.
+ auto concreteW =
+ children[2]->clone(ShadowNodeFragment{.props = concreteProps("W")});
+
+ // New: Root -> [A, W(concrete), C, Z] — Z moved to end.
+ // New flattened: [A(2), W(3), C(4), Z(7)]
+ auto newChildren =
+ std::make_shared>>();
+ newChildren->push_back(children[1]); // A
+ newChildren->push_back(concreteW); // W (concrete)
+ newChildren->push_back(children[3]); // C
+ newChildren->push_back(children[0]); // Z (moved to end)
+
+ auto newRootNode = std::static_pointer_cast(
+ rootNode->ShadowNode::clone(
+ ShadowNodeFragment{
+ .props = ShadowNodeFragment::propsPlaceholder(),
+ .children = newChildren}));
+
+ auto expected = buildStubViewTreeWithoutUsingDifferentiator(*newRootNode);
+ auto mutations = calculateShadowViewMutations(*rootNode, *newRootNode);
+ auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode);
+ viewTree.mutate(mutations);
+ EXPECT_EQ(viewTree, expected);
+}
+
+// Tests that the LIS algorithm produces correct mutations when child
+// reordering coincides with a view's flattening transition. When W(tag=3)
+// becomes flattened (loses testId), its child B(tag=6) is promoted to the
+// parent level in the flattened view. The LIS pre-computation maps old
+// children to new positions, but the flattener may consume entries from
+// newRemainingPairs during flatten/unflatten processing. This test verifies
+// the runtime newRemainingPairs check correctly handles this interaction.
+TEST_P(ShadowTreeLifecycleTest, reorderWithFlatteningTransition) {
+ auto builder = simpleComponentBuilder();
+
+ auto concreteProps = [](const std::string& id) {
+ auto props = std::make_shared();
+ props->testId = id;
+ return props;
+ };
+
+ // clang-format off
+ // Old tree: Root -> [A, W(concrete with testId, child B), C, D]
+ auto rootElement =
+ Element()
+ .tag(1)
+ .children({
+ Element().tag(2).props(concreteProps("A")),
+ Element().tag(3).props(concreteProps("W"))
+ .children({
+ Element().tag(6).props(concreteProps("B")),
+ }),
+ Element().tag(4).props(concreteProps("C")),
+ Element().tag(5).props(concreteProps("D")),
+ });
+ // clang-format on
+
+ auto rootNode = builder.build(rootElement);
+ auto children = rootNode->getChildren();
+ // children: [A(2), W(3), C(4), D(5)]
+
+ // Clone W without testId to make it non-concrete (flattened).
+ // B(tag=6) will be promoted to the parent level.
+ auto flatW = children[1]->clone(
+ ShadowNodeFragment{.props = std::make_shared()});
+
+ // New tree: Root -> [C, D, A, W(flattened)]
+ // Reorder [A,W,C,D] -> [C,D,A,W(flattened)]
+ auto reorderedChildren =
+ std::make_shared>>();
+ reorderedChildren->push_back(children[2]); // C
+ reorderedChildren->push_back(children[3]); // D
+ reorderedChildren->push_back(children[0]); // A
+ reorderedChildren->push_back(flatW); // W (flattened)
+
+ auto newRootNode = std::static_pointer_cast(
+ rootNode->ShadowNode::clone(
+ ShadowNodeFragment{
+ .props = ShadowNodeFragment::propsPlaceholder(),
+ .children = reorderedChildren}));
+
+ auto expected = buildStubViewTreeWithoutUsingDifferentiator(*newRootNode);
+ auto mutations = calculateShadowViewMutations(*rootNode, *newRootNode);
+ auto viewTree = buildStubViewTreeWithoutUsingDifferentiator(*rootNode);
+ viewTree.mutate(mutations);
+ EXPECT_EQ(viewTree, expected);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ Greedy,
+ ShadowTreeLifecycleTest,
+ ::testing::Values(false),
+ [](const auto&) { return "Greedy"; });
+
+INSTANTIATE_TEST_SUITE_P(
+ LIS,
+ ShadowTreeLifecycleTest,
+ ::testing::Values(true),
+ [](const auto&) { return "LIS"; });
diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js
index 1d6ffadb4af6..1f4108dc265d 100644
--- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js
+++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js
@@ -901,6 +901,17 @@ const definitions: FeatureFlagDefinitions = {
},
ossReleaseStage: 'none',
},
+ useLISAlgorithmInDifferentiator: {
+ defaultValue: false,
+ metadata: {
+ dateAdded: '2026-03-12',
+ description:
+ 'Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation.',
+ expectedReleaseValue: true,
+ purpose: 'experimentation',
+ },
+ ossReleaseStage: 'none',
+ },
useNativeViewConfigsInBridgelessMode: {
defaultValue: false,
metadata: {
diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js
index 3deb4d238223..3d92ce834c9f 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<<5966ef11ee71a38059decda1c529fd6f>>
+ * @generated SignedSource<>
* @flow strict
* @noformat
*/
@@ -128,6 +128,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{
updateRuntimeShadowNodeReferencesOnCommitThread: Getter,
useAlwaysAvailableJSErrorHandling: Getter,
useFabricInterop: Getter,
+ useLISAlgorithmInDifferentiator: Getter,
useNativeViewConfigsInBridgelessMode: Getter,
useNestedScrollViewAndroid: Getter,
useSharedAnimatedBackend: Getter,
@@ -529,6 +530,10 @@ export const useAlwaysAvailableJSErrorHandling: Getter = createNativeFl
* Should this application enable the Fabric Interop Layer for Android? If yes, the application will behave so that it can accept non-Fabric components and render them on Fabric. This toggle is controlling extra logic such as custom event dispatching that are needed for the Fabric Interop Layer to work correctly.
*/
export const useFabricInterop: Getter = createNativeFlagGetter('useFabricInterop', true);
+/**
+ * Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation.
+ */
+export const useLISAlgorithmInDifferentiator: Getter = createNativeFlagGetter('useLISAlgorithmInDifferentiator', false);
/**
* When enabled, the native view configs are used in bridgeless mode.
*/
diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js
index ea68b2cf9eb3..d03933b754e6 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<<85ddaee522fc3be8546aef21cf236e9a>>
+ * @generated SignedSource<<971e04eed130adab56e3306a6a26ff1c>>
* @flow strict
* @noformat
*/
@@ -105,6 +105,7 @@ export interface Spec extends TurboModule {
+updateRuntimeShadowNodeReferencesOnCommitThread?: () => boolean;
+useAlwaysAvailableJSErrorHandling?: () => boolean;
+useFabricInterop?: () => boolean;
+ +useLISAlgorithmInDifferentiator?: () => boolean;
+useNativeViewConfigsInBridgelessMode?: () => boolean;
+useNestedScrollViewAndroid?: () => boolean;
+useSharedAnimatedBackend?: () => 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 c45a6551484f..49ebf42cb3d0 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
@@ -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.
*
- * @fantom_flags enableFabricCommitBranching:*
+ * @fantom_flags enableFabricCommitBranching:* useLISAlgorithmInDifferentiator:*
* @flow strict-local
* @format
*/
@@ -14,6 +14,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
import type {HostInstance} from 'react-native';
import ensureInstance from '../../../__tests__/utilities/ensureInstance';
+import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags';
import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {View} from 'react-native';
@@ -86,6 +87,80 @@ describe('ViewFlattening', () => {
]);
});
+ /**
+ * Test worst-case reordering: moving the first child to the end.
+ *
+ * P -> [A,B,C,D,E] ==> P -> [B,C,D,E,A]
+ *
+ * The greedy two-pointer encounters A vs B mismatch at the first
+ * position and generates excessive REMOVE+INSERT pairs:
+ * 4 removes + 4 inserts = 8 mutations.
+ *
+ * The LIS algorithm identifies [B,C,D,E] as the longest increasing
+ * subsequence — those children stay in place, and only A needs to
+ * move: 1 remove + 1 insert = 2 mutations.
+ */
+ test('reordering: move first child to last (worst case for greedy)', () => {
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render(
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ root.takeMountingManagerLogs();
+
+ Fantom.runTask(() => {
+ root.render(
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ expect(root.getRenderedOutput().toJSX()).toEqual(
+
+
+
+
+
+
+ ,
+ );
+
+ const logs = root.takeMountingManagerLogs();
+ if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) {
+ // LIS = [B,C,D,E], only A moves: 1 remove + 1 insert = 2 mutations
+ expect(logs).toEqual([
+ 'Remove {type: "View", parentNativeID: "P", index: 0, nativeID: "A"}',
+ 'Insert {type: "View", parentNativeID: "P", index: 4, nativeID: "A"}',
+ ]);
+ } else {
+ // Greedy: 4 removes + 4 inserts = 8 mutations
+ expect(logs).toEqual([
+ 'Remove {type: "View", parentNativeID: "P", index: 4, nativeID: "E"}',
+ 'Remove {type: "View", parentNativeID: "P", index: 3, nativeID: "D"}',
+ 'Remove {type: "View", parentNativeID: "P", index: 2, nativeID: "C"}',
+ 'Remove {type: "View", parentNativeID: "P", index: 1, nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "P", index: 0, nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "P", index: 1, nativeID: "C"}',
+ 'Insert {type: "View", parentNativeID: "P", index: 2, nativeID: "D"}',
+ 'Insert {type: "View", parentNativeID: "P", index: 3, nativeID: "E"}',
+ ]);
+ }
+ });
+
/**
* Test reparenting mutation instruction generation.
* We cannot practically handle all possible use-cases here.
@@ -146,13 +221,23 @@ describe('ViewFlattening', () => {
,
);
- expect(root.takeMountingManagerLogs()).toEqual([
- 'Update {type: "View", nativeID: "A"}',
- 'Remove {type: "View", parentNativeID: "G", index: 0, nativeID: "A"}',
- 'Create {type: "View", nativeID: "H"}',
- 'Insert {type: "View", parentNativeID: "G", index: 0, nativeID: "H"}',
- 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}',
- ]);
+ if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "View", nativeID: "A"}',
+ 'Remove {type: "View", parentNativeID: "G", index: 0, nativeID: "A"}',
+ 'Create {type: "View", nativeID: "H"}',
+ 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}',
+ 'Insert {type: "View", parentNativeID: "G", index: 0, nativeID: "H"}',
+ ]);
+ } else {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "View", nativeID: "A"}',
+ 'Remove {type: "View", parentNativeID: "G", index: 0, nativeID: "A"}',
+ 'Create {type: "View", nativeID: "H"}',
+ 'Insert {type: "View", parentNativeID: "G", index: 0, nativeID: "H"}',
+ 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}',
+ ]);
+ }
// The view is reparented 1 level down with a different sibling
// Root -> G* -> H* -> I* -> J -> [B*, A*] [nodes with * are _not_ flattened]
@@ -182,14 +267,25 @@ describe('ViewFlattening', () => {
,
);
- expect(root.takeMountingManagerLogs()).toEqual([
- 'Remove {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}',
- 'Create {type: "View", nativeID: "I"}',
- 'Create {type: "View", nativeID: "B"}',
- 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "I"}',
- 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}',
- 'Insert {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}',
- ]);
+ if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}',
+ 'Create {type: "View", nativeID: "I"}',
+ 'Create {type: "View", nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}',
+ 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "I"}',
+ ]);
+ } else {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: "H", index: 0, nativeID: "A"}',
+ 'Create {type: "View", nativeID: "I"}',
+ 'Create {type: "View", nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "H", index: 0, nativeID: "I"}',
+ 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}',
+ ]);
+ }
// The view is reparented 1 level further down with its order with the sibling
// swapped
@@ -222,14 +318,25 @@ describe('ViewFlattening', () => {
,
);
- expect(root.takeMountingManagerLogs()).toEqual([
- 'Remove {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}',
- 'Remove {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}',
- 'Create {type: "View", nativeID: "J"}',
- 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "J"}',
- 'Insert {type: "View", parentNativeID: "J", index: 0, nativeID: "A"}',
- 'Insert {type: "View", parentNativeID: "J", index: 1, nativeID: "B"}',
- ]);
+ if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}',
+ 'Remove {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}',
+ 'Create {type: "View", nativeID: "J"}',
+ 'Insert {type: "View", parentNativeID: "J", index: 0, nativeID: "A"}',
+ 'Insert {type: "View", parentNativeID: "J", index: 1, nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "J"}',
+ ]);
+ } else {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: "I", index: 1, nativeID: "A"}',
+ 'Remove {type: "View", parentNativeID: "I", index: 0, nativeID: "B"}',
+ 'Create {type: "View", nativeID: "J"}',
+ 'Insert {type: "View", parentNativeID: "I", index: 0, nativeID: "J"}',
+ 'Insert {type: "View", parentNativeID: "J", index: 0, nativeID: "A"}',
+ 'Insert {type: "View", parentNativeID: "J", index: 1, nativeID: "B"}',
+ ]);
+ }
});
test('parent-child switching from unflattened-flattened to flattened-unflattened', () => {
@@ -340,15 +447,28 @@ describe('ViewFlattening', () => {
,
);
});
- expect(root.takeMountingManagerLogs()).toEqual([
- 'Update {type: "View", nativeID: "child"}',
- 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
- 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
- 'Delete {type: "View", nativeID: (N/A)}',
- 'Create {type: "View", nativeID: (N/A)}',
- 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
- 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
- ]);
+
+ if (ReactNativeFeatureFlags.useLISAlgorithmInDifferentiator()) {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "View", nativeID: "child"}',
+ 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
+ 'Delete {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
+ 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+ } else {
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "View", nativeID: "child"}',
+ 'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
+ 'Delete {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
+ ]);
+ }
});
test('#51378: view with rgba(255,255,255,127/256) background color is not flattened', () => {