Skip to content

Commit b03e255

Browse files
committed
Add useTraitHiddenOnIOS feature flag
## Summary: Mirrors useTraitHiddenOnAndroid from #54112. Lets iOS apps opt out of the `Trait::Hidden` slice-skip; Android can already opt in. Default is `true`, preserving today's iOS behavior. What the optimization does D22134220 (2020, "Fabric: display: none nodes do not create views anymore") added `Trait::Hidden` and a slice-skip in `sliceChildShadowNodeViewPairs` that filters out subtrees whose Yoga display is None. On iOS the diff then issues REMOVE + DELETE for the entire subtree — invisible content stops costing anything. For most UIs this is a clean win. Where it bites The optimization assumes that `display: none` transitions are rare. That breaks for a specific pattern: **a custom host component mounted under `<Suspense>`**. Each suspend → resume cycle, Suspense flips display between `flex` and `none`. With the optimization on, every cycle: - tears down the entire subtree of UIViews, - drops per-instance native state — measurement caches, scroll position, animation drivers, anything internal to the host component, - re-runs `init`/`dealloc` and rebuilds the subtree on resume. Suspend/resume ends up heavier than a fresh mount, and any state the component held disappears between renders. What the flag does **It re-activates an existing code path. Nothing in the mounting layer is new.** The hide-via-`UIView.hidden` wiring shipped in D8460108 (June 2018, "Fabric: Default support of displayType and layoutDirection layout...") and has lived in `UIView+ComponentViewProtocol updateLayoutMetrics:` ever since — a few lines below the slice consumer in the same file. For two years it was the only iOS path; the 2020 slice-skip didn't replace it, it just made it unreachable in the common case. Setting the flag to `false` lets Hidden shadow nodes pass through the slice. The differ emits an `UPDATE_LAYOUT_METRICS` mutation with `displayType == None`, and the 2018 wiring picks it up: `self.hidden = YES` on the underlying UIView. Same view, hidden in place. Defaults - `useTraitHiddenOnIOS = true` — keeps the iOS behavior introduced in 2020. - `useTraitHiddenOnAndroid = false` — keeps the Android behavior, which never adopted the slice-skip. Both flags share the same semantic ("use the optimization"); the defaults encode each platform's pre-flag behavior. Flipping either default is out of scope. ## Changelog: [IOS] [ADDED] - useTraitHiddenOnIOS feature flag to opt out of the `display: none` slice-skip optimization ## Test Plan: No new tests. `StackingContextTest` exercises the slice-skip path and passes unchanged with the flag at its default `true`. Manually flipping the flag to `false` produces the existing Android branch's expected view tree (8 views, Hidden subtrees preserved with `self.hidden = YES`).
1 parent f9f7133 commit b03e255

21 files changed

Lines changed: 146 additions & 25 deletions

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<<94a1d6bbc5595c495a2c54851a9c5b5b>>
7+
* @generated SignedSource<<a03d363d13ff18dd87d15dc223c9c32d>>
88
*/
99

1010
/**
@@ -396,6 +396,12 @@ public object ReactNativeFeatureFlags {
396396
@JvmStatic
397397
public fun useSilenceErrorSMMViewNotFound(): Boolean = accessor.useSilenceErrorSMMViewNotFound()
398398

399+
/**
400+
* iOS only. When true (default), shadow nodes carrying ShadowNodeTraits::Trait::Hidden are filtered out of the mounting slice. When false, those nodes stay in the slice and are hidden via UIView.hidden = YES in updateLayoutMetrics:.
401+
*/
402+
@JvmStatic
403+
public fun useTraitHiddenOnIOS(): Boolean = accessor.useTraitHiddenOnIOS()
404+
399405
/**
400406
* In Bridgeless mode, should legacy NativeModules use the TurboModule system?
401407
*/

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<<4e19cdce9383c287c849b912fa74586c>>
7+
* @generated SignedSource<<b3ddcd5c53e3fec3c0397dbeaa2a3b0c>>
88
*/
99

1010
/**
@@ -81,6 +81,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
8181
private var useRawPropsJsiValueCache: Boolean? = null
8282
private var useShadowNodeStateOnCloneCache: Boolean? = null
8383
private var useSilenceErrorSMMViewNotFoundCache: Boolean? = null
84+
private var useTraitHiddenOnIOSCache: Boolean? = null
8485
private var useTurboModuleInteropCache: Boolean? = null
8586
private var useTurboModulesCache: Boolean? = null
8687
private var virtualViewPrerenderRatioCache: Double? = null
@@ -635,6 +636,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
635636
return cached
636637
}
637638

639+
override fun useTraitHiddenOnIOS(): Boolean {
640+
var cached = useTraitHiddenOnIOSCache
641+
if (cached == null) {
642+
cached = ReactNativeFeatureFlagsCxxInterop.useTraitHiddenOnIOS()
643+
useTraitHiddenOnIOSCache = cached
644+
}
645+
return cached
646+
}
647+
638648
override fun useTurboModuleInterop(): Boolean {
639649
var cached = useTurboModuleInteropCache
640650
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<<2a525ef08b04eb1c244badb8cc865e4d>>
7+
* @generated SignedSource<<1234d5c7c6bb64451618817387318d13>>
88
*/
99

1010
/**
@@ -150,6 +150,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
150150

151151
@DoNotStrip @JvmStatic public external fun useSilenceErrorSMMViewNotFound(): Boolean
152152

153+
@DoNotStrip @JvmStatic public external fun useTraitHiddenOnIOS(): Boolean
154+
153155
@DoNotStrip @JvmStatic public external fun useTurboModuleInterop(): Boolean
154156

155157
@DoNotStrip @JvmStatic public external fun useTurboModules(): 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<<58b3101bd16b63268440523cdee7ae93>>
7+
* @generated SignedSource<<3255db6cb7c71d866991568dc632321f>>
88
*/
99

1010
/**
@@ -145,6 +145,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
145145

146146
override fun useSilenceErrorSMMViewNotFound(): Boolean = false
147147

148+
override fun useTraitHiddenOnIOS(): Boolean = true
149+
148150
override fun useTurboModuleInterop(): Boolean = false
149151

150152
override fun useTurboModules(): 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<<3c639d5edf52fc3feee65388b5ac3ed7>>
7+
* @generated SignedSource<<6f23f8fd399adf79814bb8f2edc29841>>
88
*/
99

1010
/**
@@ -85,6 +85,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
8585
private var useRawPropsJsiValueCache: Boolean? = null
8686
private var useShadowNodeStateOnCloneCache: Boolean? = null
8787
private var useSilenceErrorSMMViewNotFoundCache: Boolean? = null
88+
private var useTraitHiddenOnIOSCache: Boolean? = null
8889
private var useTurboModuleInteropCache: Boolean? = null
8990
private var useTurboModulesCache: Boolean? = null
9091
private var virtualViewPrerenderRatioCache: Double? = null
@@ -700,6 +701,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
700701
return cached
701702
}
702703

704+
override fun useTraitHiddenOnIOS(): Boolean {
705+
var cached = useTraitHiddenOnIOSCache
706+
if (cached == null) {
707+
cached = currentProvider.useTraitHiddenOnIOS()
708+
accessedFeatureFlags.add("useTraitHiddenOnIOS")
709+
useTraitHiddenOnIOSCache = cached
710+
}
711+
return cached
712+
}
713+
703714
override fun useTurboModuleInterop(): Boolean {
704715
var cached = useTurboModuleInteropCache
705716
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<<0a4b05562ed8b30b7d4dabd5c8680d6d>>
7+
* @generated SignedSource<<2877a4acf03951884d1d663fdf00b9f8>>
88
*/
99

1010
/**
@@ -145,6 +145,8 @@ public interface ReactNativeFeatureFlagsProvider {
145145

146146
@DoNotStrip public fun useSilenceErrorSMMViewNotFound(): Boolean
147147

148+
@DoNotStrip public fun useTraitHiddenOnIOS(): Boolean
149+
148150
@DoNotStrip public fun useTurboModuleInterop(): Boolean
149151

150152
@DoNotStrip public fun useTurboModules(): 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<<64b1d9667e6869ea4ba406222b307464>>
7+
* @generated SignedSource<<c3ce57d947ffe2b5b404acd4a8527ae0>>
88
*/
99

1010
/**
@@ -405,6 +405,12 @@ class ReactNativeFeatureFlagsJavaProvider
405405
return method(javaProvider_);
406406
}
407407

408+
bool useTraitHiddenOnIOS() override {
409+
static const auto method =
410+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("useTraitHiddenOnIOS");
411+
return method(javaProvider_);
412+
}
413+
408414
bool useTurboModuleInterop() override {
409415
static const auto method =
410416
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("useTurboModuleInterop");
@@ -738,6 +744,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useSilenceErrorSMMViewNotFound(
738744
return ReactNativeFeatureFlags::useSilenceErrorSMMViewNotFound();
739745
}
740746

747+
bool JReactNativeFeatureFlagsCxxInterop::useTraitHiddenOnIOS(
748+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
749+
return ReactNativeFeatureFlags::useTraitHiddenOnIOS();
750+
}
751+
741752
bool JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop(
742753
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
743754
return ReactNativeFeatureFlags::useTurboModuleInterop();
@@ -972,6 +983,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
972983
makeNativeMethod(
973984
"useSilenceErrorSMMViewNotFound",
974985
JReactNativeFeatureFlagsCxxInterop::useSilenceErrorSMMViewNotFound),
986+
makeNativeMethod(
987+
"useTraitHiddenOnIOS",
988+
JReactNativeFeatureFlagsCxxInterop::useTraitHiddenOnIOS),
975989
makeNativeMethod(
976990
"useTurboModuleInterop",
977991
JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop),

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<<073595986845439fdcb30edc52dadc30>>
7+
* @generated SignedSource<<f4d8c8c96e3d7075dc71b8ac048fc7e0>>
88
*/
99

1010
/**
@@ -213,6 +213,9 @@ class JReactNativeFeatureFlagsCxxInterop
213213
static bool useSilenceErrorSMMViewNotFound(
214214
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
215215

216+
static bool useTraitHiddenOnIOS(
217+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
218+
216219
static bool useTurboModuleInterop(
217220
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
218221

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<<eb8488e034b281dc5cf384aa48b0ecf0>>
7+
* @generated SignedSource<<bd604e84ed03731ac48b2c0f41b36089>>
88
*/
99

1010
/**
@@ -270,6 +270,10 @@ bool ReactNativeFeatureFlags::useSilenceErrorSMMViewNotFound() {
270270
return getAccessor().useSilenceErrorSMMViewNotFound();
271271
}
272272

273+
bool ReactNativeFeatureFlags::useTraitHiddenOnIOS() {
274+
return getAccessor().useTraitHiddenOnIOS();
275+
}
276+
273277
bool ReactNativeFeatureFlags::useTurboModuleInterop() {
274278
return getAccessor().useTurboModuleInterop();
275279
}

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

Lines changed: 6 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<<7b1258f9970366f0c068f28e2c16ce12>>
7+
* @generated SignedSource<<8dc801da5ad5fd085dcedb245f7dee08>>
88
*/
99

1010
/**
@@ -344,6 +344,11 @@ class ReactNativeFeatureFlags {
344344
*/
345345
RN_EXPORT static bool useSilenceErrorSMMViewNotFound();
346346

347+
/**
348+
* iOS only. When true (default), shadow nodes carrying ShadowNodeTraits::Trait::Hidden are filtered out of the mounting slice. When false, those nodes stay in the slice and are hidden via UIView.hidden = YES in updateLayoutMetrics:.
349+
*/
350+
RN_EXPORT static bool useTraitHiddenOnIOS();
351+
347352
/**
348353
* In Bridgeless mode, should legacy NativeModules use the TurboModule system?
349354
*/

0 commit comments

Comments
 (0)