Skip to content

Commit 8df90ef

Browse files
sammy-SCfacebook-github-bot
authored andcommitted
Add fixYogaFlexBasisFitContentInMainAxis flag to avoid unnecessary re-measurement
Summary: 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). Differential Revision: D94658492
1 parent 851e5ae commit 8df90ef

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<<2ca6b65f1103a486e4e5a006de629e76>>
7+
* @generated SignedSource<<221170890566c7b533c9703e1c278f4d>>
88
*/
99

1010
/**
@@ -378,6 +378,12 @@ public object ReactNativeFeatureFlags {
378378
@JvmStatic
379379
public fun fixTextClippingAndroid15useBoundsForWidth(): Boolean = accessor.fixTextClippingAndroid15useBoundsForWidth()
380380

381+
/**
382+
* 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.
383+
*/
384+
@JvmStatic
385+
public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean = accessor.fixYogaFlexBasisFitContentInMainAxis()
386+
381387
/**
382388
* 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`.
383389
*/

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<<202662ab1c26ed104cfe837162d4f9a2>>
7+
* @generated SignedSource<<7e8b6123d3aa6706409459490c97f03a>>
88
*/
99

1010
/**
@@ -78,6 +78,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
7878
private var fixFindShadowNodeByTagRaceConditionCache: Boolean? = null
7979
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
8080
private var fixTextClippingAndroid15useBoundsForWidthCache: Boolean? = null
81+
private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null
8182
private var fuseboxAssertSingleHostStateCache: Boolean? = null
8283
private var fuseboxEnabledReleaseCache: Boolean? = null
8384
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
@@ -629,6 +630,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
629630
return cached
630631
}
631632

633+
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean {
634+
var cached = fixYogaFlexBasisFitContentInMainAxisCache
635+
if (cached == null) {
636+
cached = ReactNativeFeatureFlagsCxxInterop.fixYogaFlexBasisFitContentInMainAxis()
637+
fixYogaFlexBasisFitContentInMainAxisCache = cached
638+
}
639+
return cached
640+
}
641+
632642
override fun fuseboxAssertSingleHostState(): Boolean {
633643
var cached = fuseboxAssertSingleHostStateCache
634644
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<<03f5b76fefda14757a43414f6624601a>>
7+
* @generated SignedSource<<f2c8850b294c6d81274ac0b59f4f0ec3>>
88
*/
99

1010
/**
@@ -144,6 +144,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
144144

145145
@DoNotStrip @JvmStatic public external fun fixTextClippingAndroid15useBoundsForWidth(): Boolean
146146

147+
@DoNotStrip @JvmStatic public external fun fixYogaFlexBasisFitContentInMainAxis(): Boolean
148+
147149
@DoNotStrip @JvmStatic public external fun fuseboxAssertSingleHostState(): Boolean
148150

149151
@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<<7713ee7b0947f0ae8c66b73413a7f226>>
7+
* @generated SignedSource<<3e7f4ec0f058f7ba26e72c999f9c5d0c>>
88
*/
99

1010
/**
@@ -139,6 +139,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
139139

140140
override fun fixTextClippingAndroid15useBoundsForWidth(): Boolean = false
141141

142+
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean = false
143+
142144
override fun fuseboxAssertSingleHostState(): Boolean = true
143145

144146
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<<79cf67c605a76059f010cf2ccd0ec64b>>
7+
* @generated SignedSource<<001ba92887aaf2db3d3c9247518962c1>>
88
*/
99

1010
/**
@@ -82,6 +82,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
8282
private var fixFindShadowNodeByTagRaceConditionCache: Boolean? = null
8383
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
8484
private var fixTextClippingAndroid15useBoundsForWidthCache: Boolean? = null
85+
private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null
8586
private var fuseboxAssertSingleHostStateCache: Boolean? = null
8687
private var fuseboxEnabledReleaseCache: Boolean? = null
8788
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
@@ -691,6 +692,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
691692
return cached
692693
}
693694

695+
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean {
696+
var cached = fixYogaFlexBasisFitContentInMainAxisCache
697+
if (cached == null) {
698+
cached = currentProvider.fixYogaFlexBasisFitContentInMainAxis()
699+
accessedFeatureFlags.add("fixYogaFlexBasisFitContentInMainAxis")
700+
fixYogaFlexBasisFitContentInMainAxisCache = cached
701+
}
702+
return cached
703+
}
704+
694705
override fun fuseboxAssertSingleHostState(): Boolean {
695706
var cached = fuseboxAssertSingleHostStateCache
696707
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<<655d1ff3caeb3d8e95d44176d65b951b>>
7+
* @generated SignedSource<<c6f68de358f4748b8bc6eac8ec0fefd6>>
88
*/
99

1010
/**
@@ -139,6 +139,8 @@ public interface ReactNativeFeatureFlagsProvider {
139139

140140
@DoNotStrip public fun fixTextClippingAndroid15useBoundsForWidth(): Boolean
141141

142+
@DoNotStrip public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean
143+
142144
@DoNotStrip public fun fuseboxAssertSingleHostState(): Boolean
143145

144146
@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<<175e48107d9bd1d713a4da0a252a58bf>>
7+
* @generated SignedSource<<ca623df2cb1859a07c626f5847cbe203>>
88
*/
99

1010
/**
@@ -387,6 +387,12 @@ class ReactNativeFeatureFlagsJavaProvider
387387
return method(javaProvider_);
388388
}
389389

390+
bool fixYogaFlexBasisFitContentInMainAxis() override {
391+
static const auto method =
392+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("fixYogaFlexBasisFitContentInMainAxis");
393+
return method(javaProvider_);
394+
}
395+
390396
bool fuseboxAssertSingleHostState() override {
391397
static const auto method =
392398
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("fuseboxAssertSingleHostState");
@@ -849,6 +855,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fixTextClippingAndroid15useBoundsForWid
849855
return ReactNativeFeatureFlags::fixTextClippingAndroid15useBoundsForWidth();
850856
}
851857

858+
bool JReactNativeFeatureFlagsCxxInterop::fixYogaFlexBasisFitContentInMainAxis(
859+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
860+
return ReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis();
861+
}
862+
852863
bool JReactNativeFeatureFlagsCxxInterop::fuseboxAssertSingleHostState(
853864
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
854865
return ReactNativeFeatureFlags::fuseboxAssertSingleHostState();
@@ -1194,6 +1205,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
11941205
makeNativeMethod(
11951206
"fixTextClippingAndroid15useBoundsForWidth",
11961207
JReactNativeFeatureFlagsCxxInterop::fixTextClippingAndroid15useBoundsForWidth),
1208+
makeNativeMethod(
1209+
"fixYogaFlexBasisFitContentInMainAxis",
1210+
JReactNativeFeatureFlagsCxxInterop::fixYogaFlexBasisFitContentInMainAxis),
11971211
makeNativeMethod(
11981212
"fuseboxAssertSingleHostState",
11991213
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<<e182e4c23748be5ade70f6568ea2046f>>
7+
* @generated SignedSource<<eba2da5eaffcbe9001b75a0bd0792959>>
88
*/
99

1010
/**
@@ -204,6 +204,9 @@ class JReactNativeFeatureFlagsCxxInterop
204204
static bool fixTextClippingAndroid15useBoundsForWidth(
205205
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
206206

207+
static bool fixYogaFlexBasisFitContentInMainAxis(
208+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
209+
207210
static bool fuseboxAssertSingleHostState(
208211
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
209212

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<<5208616bc5d84f040ad0fa5e85acd6c4>>
7+
* @generated SignedSource<<505c017bd9d3eb1a07ec4602744ad55c>>
88
*/
99

1010
/**
@@ -258,6 +258,10 @@ bool ReactNativeFeatureFlags::fixTextClippingAndroid15useBoundsForWidth() {
258258
return getAccessor().fixTextClippingAndroid15useBoundsForWidth();
259259
}
260260

261+
bool ReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis() {
262+
return getAccessor().fixYogaFlexBasisFitContentInMainAxis();
263+
}
264+
261265
bool ReactNativeFeatureFlags::fuseboxAssertSingleHostState() {
262266
return getAccessor().fuseboxAssertSingleHostState();
263267
}

0 commit comments

Comments
 (0)