Skip to content

Commit 77231f4

Browse files
sammy-SCmeta-codesync[bot]
authored andcommitted
Add fixYogaFlexBasisFitContentInMainAxis flag to avoid unnecessary re-measurement (#55897)
Summary: X-link: react/yoga#1909 Pull Request resolved: #55897 changelog: [internal] this change is gated. ## Problem When Yoga computes flex basis for container children, the legacy behavior applies a `FitContent` constraint in the **main axis**, bounding the child's measurement by the parent's available space. This creates a dependency between the child's flex basis and the parent's content-determined size, causing **unnecessary re-measurement and cascading ownership clones** when siblings change size. ### The re-measurement cascade (before fix) ``` ScrollView (overflow: scroll) +-------------------------------+ | Content Container (auto h) | | +---------------------------+ | | | Item A h=200 | | <-- Item A height changes | +---------------------------+ | | | Item B h=300 | | | +---------------------------+ | | | Item C h=150 | | | +---------------------------+ | +-------------------------------+ | v Content container height changes (200+300+150 = 650) | v FitContent(650) re-measures ALL items <-- PROBLEM | because their flex basis was FitContent(old_height) v Cascading clones of the entire subtree ``` With the legacy `FitContent` in the main axis, each item's flex basis is `min(content, parent_height)`. When Item A changes height, the content container's height changes, which invalidates the FitContent constraint for ALL items, triggering a full re-measurement cascade. ### After fix (MaxContent in main axis) ``` ScrollView (overflow: scroll) +-------------------------------+ | Content Container (auto h) | | +---------------------------+ | | | Item A h=200 -> 250 | | <-- Item A height changes | +---------------------------+ | | | Item B h=300 | | <-- NOT re-measured (basis unchanged) | +---------------------------+ | | | Item C h=150 | | <-- NOT re-measured (basis unchanged) | +---------------------------+ | +-------------------------------+ | v Content container height changes (250+300+150 = 700) | v Only Item A is re-measured. B and C keep their MaxContent flex basis (independent of parent height). ``` With `MaxContent`, each item's flex basis is its intrinsic content size, independent of the parent. Changing one item doesn't invalidate siblings. ## Solution This diff adds a `FlexBasisFitContentInMainAxis` errata bit gated by the `fixYogaFlexBasisFitContentInMainAxis` feature flag. When the fix is active, flex basis measurement uses `MaxContent` (unbounded) instead of `FitContent` for container children in the main axis. ### Three check points in `computeFlexBasisForChild` ``` computeFlexBasisForChild(parent, child) | |-- Check 1: Accept positive flex basis when mainAxisSize is NaN | (fixes flexBasis:200 items in ScrollView getting height 0) | |-- Check 2: FitContent vs MaxContent constraint | +--------------------------------------------------+ | | Parent type | Legacy (errata) | Fix | | |--------------------+-----------------+-----------| | | Auto height | FitContent | MaxContent| <-- key change | | Definite height | FitContent | FitContent| <-- preserved | | Scroll container | MaxContent | MaxContent| <-- unchanged | | Text child (any) | FitContent | FitContent| <-- preserved | +--------------------------------------------------+ | |-- Check 3: ownerHeightForChildren fallback (preserves percentage resolution when availableInnerHeight is NaN) ``` ### Why definite-height parents keep FitContent Yoga's default `flexShrink` is 0 (unlike CSS's default of 1). Without FitContent, a child measured at MaxContent would get a flex basis equal to its full content height and never shrink to fit: ``` View (height: 760) View (height: 760) +-------------------+ +-------------------+ | Wrapper (auto h) | | Wrapper (auto h) | | +-----------+ | | +-----------+-----|----+ | | ScrollView| | | | ScrollView| | | | | content: | | | | content: | | | | | 1800px | | | | 1800px | | | | +-----------+ | | | | | | | h=760 (bounded) | | +-----------+ | | +-------------------+ +---|---------+-----|----+ FitContent: wrapper=760 | h=1800 (overflows!) ScrollView can scroll MaxContent: wrapper=1800 flexShrink=0, no shrinking ScrollView frame=1800=content scrollable range = 0! ``` For **definite-height** parents, FitContent is safe (the parent's size is fixed, so no re-measurement cascade). For **auto-height** parents, MaxContent is used to avoid the cascade. ### Percentage resolution preservation (Check 3) When MaxContent is used, `availableInnerHeight` becomes NaN. This would break percentage-height grandchildren. Check 3 derives a definite `ownerHeightForChildren` from the parent-provided `ownerHeight`: ``` View (height: 844) +---------------------------+ | Wrapper (auto h) | availableInnerHeight = NaN (MaxContent) | ownerHeight = 844 | ownerHeightForChildren = 844 (from Check 3) | +-----+ +-----------+ | | |h:500| |h:'50%' | | 50% resolves against 844, not NaN | | | |= 422 | | | +-----+ +-----------+ | +---------------------------+ ``` Children of scroll containers skip this fallback (scroll content is intentionally unbounded). Reviewed By: javache Differential Revision: D94658492 fbshipit-source-id: 1587151670803ace0eae2ee91883fe4be72bfa27
1 parent 63796a9 commit 77231f4

27 files changed

Lines changed: 286 additions & 68 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<<38f50776ad2b88fa69b710dbd67315e2>>
7+
* @generated SignedSource<<66d3b8ed600077a287d6d7d8f6230b7b>>
88
*/
99

1010
/**
@@ -384,6 +384,12 @@ public object ReactNativeFeatureFlags {
384384
@JvmStatic
385385
public fun fixTextClippingAndroid15useBoundsForWidth(): Boolean = accessor.fixTextClippingAndroid15useBoundsForWidth()
386386

387+
/**
388+
* When enabled, Yoga will not apply a FitContent constraint in the main axis during flex basis computation for non-measure container nodes. This prevents unnecessary re-measurement and cascading clones when a sibling changes size in a ScrollView.
389+
*/
390+
@JvmStatic
391+
public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean = accessor.fixYogaFlexBasisFitContentInMainAxis()
392+
387393
/**
388394
* Enable system assertion validating that Fusebox is configured with a single host. When set, the CDP backend will dynamically disable features (Perf and Network) in the event that multiple hosts are registered (undefined behaviour), and broadcast this over `ReactNativeApplication.systemStateChanged`.
389395
*/

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<<e831db22fd6d79e3caff459a6058e297>>
7+
* @generated SignedSource<<98c5f6184864381e579a9df82741928b>>
88
*/
99

1010
/**
@@ -79,6 +79,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
7979
private var fixFindShadowNodeByTagRaceConditionCache: Boolean? = null
8080
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
8181
private var fixTextClippingAndroid15useBoundsForWidthCache: Boolean? = null
82+
private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null
8283
private var fuseboxAssertSingleHostStateCache: Boolean? = null
8384
private var fuseboxEnabledReleaseCache: Boolean? = null
8485
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
@@ -639,6 +640,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
639640
return cached
640641
}
641642

643+
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean {
644+
var cached = fixYogaFlexBasisFitContentInMainAxisCache
645+
if (cached == null) {
646+
cached = ReactNativeFeatureFlagsCxxInterop.fixYogaFlexBasisFitContentInMainAxis()
647+
fixYogaFlexBasisFitContentInMainAxisCache = cached
648+
}
649+
return cached
650+
}
651+
642652
override fun fuseboxAssertSingleHostState(): Boolean {
643653
var cached = fuseboxAssertSingleHostStateCache
644654
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<<85d23b5f33a6a09c1e42b976a3a99f2f>>
7+
* @generated SignedSource<<2bf2a1dec031145181f6bc28c50cf32a>>
88
*/
99

1010
/**
@@ -146,6 +146,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
146146

147147
@DoNotStrip @JvmStatic public external fun fixTextClippingAndroid15useBoundsForWidth(): Boolean
148148

149+
@DoNotStrip @JvmStatic public external fun fixYogaFlexBasisFitContentInMainAxis(): Boolean
150+
149151
@DoNotStrip @JvmStatic public external fun fuseboxAssertSingleHostState(): Boolean
150152

151153
@DoNotStrip @JvmStatic public external fun fuseboxEnabledRelease(): 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<<4adf4e6f40b726bf9d3680301fb92e1f>>
7+
* @generated SignedSource<<89cd07b543fc079341bb3a5d90e50d13>>
88
*/
99

1010
/**
@@ -141,6 +141,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
141141

142142
override fun fixTextClippingAndroid15useBoundsForWidth(): Boolean = false
143143

144+
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean = false
145+
144146
override fun fuseboxAssertSingleHostState(): Boolean = true
145147

146148
override fun fuseboxEnabledRelease(): 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<<2115ff3be1f3944d499090d3089d185b>>
7+
* @generated SignedSource<<e82235d73386509fc2378a5b8e4b2cfa>>
88
*/
99

1010
/**
@@ -83,6 +83,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
8383
private var fixFindShadowNodeByTagRaceConditionCache: Boolean? = null
8484
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
8585
private var fixTextClippingAndroid15useBoundsForWidthCache: Boolean? = null
86+
private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null
8687
private var fuseboxAssertSingleHostStateCache: Boolean? = null
8788
private var fuseboxEnabledReleaseCache: Boolean? = null
8889
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
@@ -702,6 +703,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
702703
return cached
703704
}
704705

706+
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean {
707+
var cached = fixYogaFlexBasisFitContentInMainAxisCache
708+
if (cached == null) {
709+
cached = currentProvider.fixYogaFlexBasisFitContentInMainAxis()
710+
accessedFeatureFlags.add("fixYogaFlexBasisFitContentInMainAxis")
711+
fixYogaFlexBasisFitContentInMainAxisCache = cached
712+
}
713+
return cached
714+
}
715+
705716
override fun fuseboxAssertSingleHostState(): Boolean {
706717
var cached = fuseboxAssertSingleHostStateCache
707718
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<<1bbbe280815927e0f977f1d1e7787937>>
7+
* @generated SignedSource<<e3051a7aec6e7da31a65eb2d58fcd64b>>
88
*/
99

1010
/**
@@ -141,6 +141,8 @@ public interface ReactNativeFeatureFlagsProvider {
141141

142142
@DoNotStrip public fun fixTextClippingAndroid15useBoundsForWidth(): Boolean
143143

144+
@DoNotStrip public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean
145+
144146
@DoNotStrip public fun fuseboxAssertSingleHostState(): Boolean
145147

146148
@DoNotStrip public fun fuseboxEnabledRelease(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaErrata.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public enum YogaErrata {
1414
STRETCH_FLEX_BASIS(1),
1515
ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING(2),
1616
ABSOLUTE_PERCENT_AGAINST_INNER_SIZE(4),
17+
FLEX_BASIS_FIT_CONTENT_IN_MAIN_AXIS(8),
1718
ALL(2147483647),
1819
CLASSIC(2147483646);
1920

@@ -33,6 +34,7 @@ public static YogaErrata fromInt(int value) {
3334
case 1: return STRETCH_FLEX_BASIS;
3435
case 2: return ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING;
3536
case 4: return ABSOLUTE_PERCENT_AGAINST_INNER_SIZE;
37+
case 8: return FLEX_BASIS_FIT_CONTENT_IN_MAIN_AXIS;
3638
case 2147483647: return ALL;
3739
case 2147483646: return CLASSIC;
3840
default: throw new IllegalArgumentException("Unknown enum value: " + value);

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<<b2033680b5b3f22a679022e3fd7edbc0>>
7+
* @generated SignedSource<<95380ca380ce7c5d3d2293b105d42247>>
88
*/
99

1010
/**
@@ -393,6 +393,12 @@ class ReactNativeFeatureFlagsJavaProvider
393393
return method(javaProvider_);
394394
}
395395

396+
bool fixYogaFlexBasisFitContentInMainAxis() override {
397+
static const auto method =
398+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("fixYogaFlexBasisFitContentInMainAxis");
399+
return method(javaProvider_);
400+
}
401+
396402
bool fuseboxAssertSingleHostState() override {
397403
static const auto method =
398404
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("fuseboxAssertSingleHostState");
@@ -860,6 +866,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fixTextClippingAndroid15useBoundsForWid
860866
return ReactNativeFeatureFlags::fixTextClippingAndroid15useBoundsForWidth();
861867
}
862868

869+
bool JReactNativeFeatureFlagsCxxInterop::fixYogaFlexBasisFitContentInMainAxis(
870+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
871+
return ReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis();
872+
}
873+
863874
bool JReactNativeFeatureFlagsCxxInterop::fuseboxAssertSingleHostState(
864875
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
865876
return ReactNativeFeatureFlags::fuseboxAssertSingleHostState();
@@ -1208,6 +1219,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
12081219
makeNativeMethod(
12091220
"fixTextClippingAndroid15useBoundsForWidth",
12101221
JReactNativeFeatureFlagsCxxInterop::fixTextClippingAndroid15useBoundsForWidth),
1222+
makeNativeMethod(
1223+
"fixYogaFlexBasisFitContentInMainAxis",
1224+
JReactNativeFeatureFlagsCxxInterop::fixYogaFlexBasisFitContentInMainAxis),
12111225
makeNativeMethod(
12121226
"fuseboxAssertSingleHostState",
12131227
JReactNativeFeatureFlagsCxxInterop::fuseboxAssertSingleHostState),

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<<78e35f9316b9568e7a44153d542b6ebd>>
7+
* @generated SignedSource<<72294df9c57d40232a21ab83bcbd12cc>>
88
*/
99

1010
/**
@@ -207,6 +207,9 @@ class JReactNativeFeatureFlagsCxxInterop
207207
static bool fixTextClippingAndroid15useBoundsForWidth(
208208
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
209209

210+
static bool fixYogaFlexBasisFitContentInMainAxis(
211+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
212+
210213
static bool fuseboxAssertSingleHostState(
211214
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
212215

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<<8643070722a65532f6c928ed82ca2aa5>>
7+
* @generated SignedSource<<faa098824843d977ad6207d243284603>>
88
*/
99

1010
/**
@@ -262,6 +262,10 @@ bool ReactNativeFeatureFlags::fixTextClippingAndroid15useBoundsForWidth() {
262262
return getAccessor().fixTextClippingAndroid15useBoundsForWidth();
263263
}
264264

265+
bool ReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis() {
266+
return getAccessor().fixYogaFlexBasisFitContentInMainAxis();
267+
}
268+
265269
bool ReactNativeFeatureFlags::fuseboxAssertSingleHostState() {
266270
return getAccessor().fuseboxAssertSingleHostState();
267271
}

0 commit comments

Comments
 (0)