Skip to content

Commit 7e71924

Browse files
javachefacebook-github-bot
authored andcommitted
Add LIS-based differentiator for minimal child reordering mutations
Summary: The current Differentiator Stage 4 uses a greedy two-pointer algorithm to reconcile reordered children. When children are shuffled, it produces excessive REMOVE+INSERT pairs because it doesn't find the minimal edit. This adds an alternative code path that uses Longest Increasing Subsequence (LIS) to identify which children can stay in place vs which need to be moved. Items in the LIS maintain their relative order — only items outside the LIS need REMOVE+INSERT. Example: moving last element to front [A,B,C,D,E] → [E,A,B,C,D]: - Greedy: 4 REMOVEs + 5 INSERTs = 9 mutations - LIS: LIS=[A,B,C,D], only E moves = 1 REMOVE + 1 INSERT = 2 mutations The LIS algorithm is O(n log n) time, O(n) space. Since average child count is <10, the position mapping uses linear scan instead of hash tables. Guarded by `useLISAlgorithmInDifferentiator` feature flag (default off). Changelog: [Internal] Differential Revision: D96334873
1 parent 3a2d5a2 commit 7e71924

26 files changed

+1134
-83
lines changed

packages/react-native/Libraries/Components/View/__tests__/View-benchmark-itest.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7+
* @fantom_flags useLISAlgorithmInDifferentiator:*
78
* @flow strict-local
89
* @format
910
*/
@@ -173,4 +174,98 @@ Fantom.unstable_benchmark
173174
root.destroy();
174175
},
175176
}),
177+
)
178+
.test.each(
179+
[10, 50, 100],
180+
n => `reorder ${n.toString()} children (move first to last)`,
181+
() => {
182+
Fantom.runTask(() => root.render(testViews));
183+
},
184+
n => {
185+
let original: React.MixedElement;
186+
let reordered: React.MixedElement;
187+
return {
188+
beforeAll: () => {
189+
const children = [];
190+
for (let i = 0; i < n; i++) {
191+
children.push(
192+
<View
193+
key={i}
194+
collapsable={false}
195+
nativeID={`child-${i.toString()}`}
196+
style={{width: i + 1, height: i + 1}}
197+
/>,
198+
);
199+
}
200+
original = (
201+
<View collapsable={false} nativeID="parent">
202+
{children}
203+
</View>
204+
);
205+
// Move first child to last
206+
const reorderedChildren = [...children.slice(1), children[0]];
207+
reordered = (
208+
<View collapsable={false} nativeID="parent">
209+
{reorderedChildren}
210+
</View>
211+
);
212+
},
213+
beforeEach: () => {
214+
root = Fantom.createRoot();
215+
Fantom.runTask(() => root.render(original));
216+
// $FlowExpectedError[incompatible-type]
217+
testViews = reordered;
218+
},
219+
afterEach: () => {
220+
root.destroy();
221+
},
222+
};
223+
},
224+
)
225+
.test.each(
226+
[10, 50, 100],
227+
n => `reorder ${n.toString()} children (swap first two)`,
228+
() => {
229+
Fantom.runTask(() => root.render(testViews));
230+
},
231+
n => {
232+
let original: React.MixedElement;
233+
let reordered: React.MixedElement;
234+
return {
235+
beforeAll: () => {
236+
const children = [];
237+
for (let i = 0; i < n; i++) {
238+
children.push(
239+
<View
240+
key={i}
241+
collapsable={false}
242+
nativeID={`child-${i.toString()}`}
243+
style={{width: i + 1, height: i + 1}}
244+
/>,
245+
);
246+
}
247+
original = (
248+
<View collapsable={false} nativeID="parent">
249+
{children}
250+
</View>
251+
);
252+
// Swap first two children — both algorithms handle this equally
253+
const swapped = [children[1], children[0], ...children.slice(2)];
254+
reordered = (
255+
<View collapsable={false} nativeID="parent">
256+
{swapped}
257+
</View>
258+
);
259+
},
260+
beforeEach: () => {
261+
root = Fantom.createRoot();
262+
Fantom.runTask(() => root.render(original));
263+
// $FlowExpectedError[incompatible-type]
264+
testViews = reordered;
265+
},
266+
afterEach: () => {
267+
root.destroy();
268+
},
269+
};
270+
},
176271
);

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<5144fb0350b71394206d614c68ef87f0>>
7+
* @generated SignedSource<<61964fd9ddf11ed5c2848da3f4d0b490>>
88
*/
99

1010
/**
@@ -510,6 +510,12 @@ public object ReactNativeFeatureFlags {
510510
@JvmStatic
511511
public fun useFabricInterop(): Boolean = accessor.useFabricInterop()
512512

513+
/**
514+
* Use Longest Increasing Subsequence algorithm in the Differentiator to minimize REMOVE/INSERT mutations during child list reconciliation.
515+
*/
516+
@JvmStatic
517+
public fun useLISAlgorithmInDifferentiator(): Boolean = accessor.useLISAlgorithmInDifferentiator()
518+
513519
/**
514520
* When enabled, the native view configs are used in bridgeless mode.
515521
*/

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<d284f066f036908977797a381a044dfa>>
7+
* @generated SignedSource<<4ce2605ff71e60b6096a211ff902e994>>
88
*/
99

1010
/**
@@ -100,6 +100,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
100100
private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null
101101
private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null
102102
private var useFabricInteropCache: Boolean? = null
103+
private var useLISAlgorithmInDifferentiatorCache: Boolean? = null
103104
private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null
104105
private var useNestedScrollViewAndroidCache: Boolean? = null
105106
private var useSharedAnimatedBackendCache: Boolean? = null
@@ -831,6 +832,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
831832
return cached
832833
}
833834

835+
override fun useLISAlgorithmInDifferentiator(): Boolean {
836+
var cached = useLISAlgorithmInDifferentiatorCache
837+
if (cached == null) {
838+
cached = ReactNativeFeatureFlagsCxxInterop.useLISAlgorithmInDifferentiator()
839+
useLISAlgorithmInDifferentiatorCache = cached
840+
}
841+
return cached
842+
}
843+
834844
override fun useNativeViewConfigsInBridgelessMode(): Boolean {
835845
var cached = useNativeViewConfigsInBridgelessModeCache
836846
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<de1c66a540520cd88c4d358ba30f2c6d>>
7+
* @generated SignedSource<<5f3573c9983cb54c5df527a3053ebbae>>
88
*/
99

1010
/**
@@ -188,6 +188,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
188188

189189
@DoNotStrip @JvmStatic public external fun useFabricInterop(): Boolean
190190

191+
@DoNotStrip @JvmStatic public external fun useLISAlgorithmInDifferentiator(): Boolean
192+
191193
@DoNotStrip @JvmStatic public external fun useNativeViewConfigsInBridgelessMode(): Boolean
192194

193195
@DoNotStrip @JvmStatic public external fun useNestedScrollViewAndroid(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<ab3b1d2277b8cc9db1708ef94515fb35>>
7+
* @generated SignedSource<<67d638f79b7b06a087f63563c2e5ff95>>
88
*/
99

1010
/**
@@ -183,6 +183,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
183183

184184
override fun useFabricInterop(): Boolean = true
185185

186+
override fun useLISAlgorithmInDifferentiator(): Boolean = false
187+
186188
override fun useNativeViewConfigsInBridgelessMode(): Boolean = false
187189

188190
override fun useNestedScrollViewAndroid(): Boolean = false

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<7b87f5541ecf881d8ce51c5edd5b99b0>>
7+
* @generated SignedSource<<e211fdbb92579b05507ea84ae42f22ff>>
88
*/
99

1010
/**
@@ -104,6 +104,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
104104
private var updateRuntimeShadowNodeReferencesOnCommitThreadCache: Boolean? = null
105105
private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null
106106
private var useFabricInteropCache: Boolean? = null
107+
private var useLISAlgorithmInDifferentiatorCache: Boolean? = null
107108
private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null
108109
private var useNestedScrollViewAndroidCache: Boolean? = null
109110
private var useSharedAnimatedBackendCache: Boolean? = null
@@ -915,6 +916,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
915916
return cached
916917
}
917918

919+
override fun useLISAlgorithmInDifferentiator(): Boolean {
920+
var cached = useLISAlgorithmInDifferentiatorCache
921+
if (cached == null) {
922+
cached = currentProvider.useLISAlgorithmInDifferentiator()
923+
accessedFeatureFlags.add("useLISAlgorithmInDifferentiator")
924+
useLISAlgorithmInDifferentiatorCache = cached
925+
}
926+
return cached
927+
}
928+
918929
override fun useNativeViewConfigsInBridgelessMode(): Boolean {
919930
var cached = useNativeViewConfigsInBridgelessModeCache
920931
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<f34b257861830bae7012fc5201904831>>
7+
* @generated SignedSource<<540700c0c0f3259a093a98ad639478ba>>
88
*/
99

1010
/**
@@ -183,6 +183,8 @@ public interface ReactNativeFeatureFlagsProvider {
183183

184184
@DoNotStrip public fun useFabricInterop(): Boolean
185185

186+
@DoNotStrip public fun useLISAlgorithmInDifferentiator(): Boolean
187+
186188
@DoNotStrip public fun useNativeViewConfigsInBridgelessMode(): Boolean
187189

188190
@DoNotStrip public fun useNestedScrollViewAndroid(): Boolean

packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<b4670c40175b42e04eb8f03b752a0c00>>
7+
* @generated SignedSource<<6c088ccf18868fc6e54d83c7483b6607>>
88
*/
99

1010
/**
@@ -519,6 +519,12 @@ class ReactNativeFeatureFlagsJavaProvider
519519
return method(javaProvider_);
520520
}
521521

522+
bool useLISAlgorithmInDifferentiator() override {
523+
static const auto method =
524+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("useLISAlgorithmInDifferentiator");
525+
return method(javaProvider_);
526+
}
527+
522528
bool useNativeViewConfigsInBridgelessMode() override {
523529
static const auto method =
524530
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("useNativeViewConfigsInBridgelessMode");
@@ -983,6 +989,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useFabricInterop(
983989
return ReactNativeFeatureFlags::useFabricInterop();
984990
}
985991

992+
bool JReactNativeFeatureFlagsCxxInterop::useLISAlgorithmInDifferentiator(
993+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
994+
return ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator();
995+
}
996+
986997
bool JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode(
987998
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
988999
return ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode();
@@ -1304,6 +1315,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
13041315
makeNativeMethod(
13051316
"useFabricInterop",
13061317
JReactNativeFeatureFlagsCxxInterop::useFabricInterop),
1318+
makeNativeMethod(
1319+
"useLISAlgorithmInDifferentiator",
1320+
JReactNativeFeatureFlagsCxxInterop::useLISAlgorithmInDifferentiator),
13071321
makeNativeMethod(
13081322
"useNativeViewConfigsInBridgelessMode",
13091323
JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode),

packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<5433b4a2f4a0574591a38017422edac8>>
7+
* @generated SignedSource<<73cfe749b34b786e25b683c499889e48>>
88
*/
99

1010
/**
@@ -270,6 +270,9 @@ class JReactNativeFeatureFlagsCxxInterop
270270
static bool useFabricInterop(
271271
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
272272

273+
static bool useLISAlgorithmInDifferentiator(
274+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
275+
273276
static bool useNativeViewConfigsInBridgelessMode(
274277
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
275278

packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<bfb72cee88230c56d2c101be808ef300>>
7+
* @generated SignedSource<<a33f1aaed390cc914a55bd48906f1a11>>
88
*/
99

1010
/**
@@ -346,6 +346,10 @@ bool ReactNativeFeatureFlags::useFabricInterop() {
346346
return getAccessor().useFabricInterop();
347347
}
348348

349+
bool ReactNativeFeatureFlags::useLISAlgorithmInDifferentiator() {
350+
return getAccessor().useLISAlgorithmInDifferentiator();
351+
}
352+
349353
bool ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode() {
350354
return getAccessor().useNativeViewConfigsInBridgelessMode();
351355
}

0 commit comments

Comments
 (0)