From 1c0f54ecb8eedcf0d731bd181d72f423d1d11226 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Thu, 29 Oct 2026 19:22:54 +0100 Subject: [PATCH 1/2] Avoid per-child dynamic_casts during Yoga layout `resolveErrata` and `getContentBounds` check whether a child is a `LayoutConformanceShadowNode` or a `ViewShadowNode` with a `dynamic_cast`, which fails for almost every node. Both classes are final, so compare the component handle instead. `shadowNodeFromContext` casts the Yoga node's context, which only this class's constructors set (to `this`), so a `static_cast` recovers it. Co-Authored-By: Claude Opus 5.5 --- .../view/YogaLayoutableShadowNode.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp index 226385a6ec3..ce9951fe4a2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -560,9 +560,13 @@ void YogaLayoutableShadowNode::configureYogaTree( } YGErrata YogaLayoutableShadowNode::resolveErrata(YGErrata defaultErrata) const { - if (auto layoutConformanceNode = - dynamic_cast(this)) { - switch (layoutConformanceNode->getConcreteProps().mode) { + // This runs for every child on every layout, and almost no node is a + // LayoutConformance node, so a failing `dynamic_cast` would be the common + // case. The class is final, so comparing component handles is equivalent. + if (getComponentHandle() == LayoutConformanceShadowNode::Handle()) { + const auto& layoutConformanceNode = + static_cast(*this); + switch (layoutConformanceNode.getConcreteProps().mode) { case LayoutConformance::Strict: // CSS Flexbox §4.5 automatic minimum sizing is the spec-compliant // behaviour, so strict conformance should adopt it — but its probe @@ -829,7 +833,12 @@ Rect YogaLayoutableShadowNode::getContentBounds() const { auto layoutMetricsWithOverflowInset = childNode.getLayoutMetrics(); if (layoutMetricsWithOverflowInset.displayType != DisplayType::None) { - auto viewChildNode = dynamic_cast(&childNode); + // ViewShadowNode is final, so comparing component handles is equivalent + // to a `dynamic_cast`, without its cost for every non-View child. + auto viewChildNode = + childNode.getComponentHandle() == ViewShadowNode::Handle() + ? static_cast(&childNode) + : nullptr; auto hitSlop = viewChildNode != nullptr ? viewChildNode->getConcreteProps().hitSlop : EdgeInsets{}; @@ -951,8 +960,8 @@ float YogaLayoutableShadowNode::yogaNodeBaselineCallbackConnector( YogaLayoutableShadowNode& YogaLayoutableShadowNode::shadowNodeFromContext( YGNodeConstRef yogaNode) { - return dynamic_cast( - *static_cast(YGNodeGetContext(yogaNode))); + // The context is only set by the constructors of this class, to `this`. + return *static_cast(YGNodeGetContext(yogaNode)); } yoga::Config& YogaLayoutableShadowNode::initializeYogaConfig( From e8bb8b36cb349ffcdc4aaaea002206580fdb5b85 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 30 Oct 2026 11:56:42 +0100 Subject: [PATCH 2/2] Define the LayoutConformance component name once `LayoutConformanceShadowNodeComponentName` was a `constexpr` array in the header, so every translation unit had its own copy. `Handle()` is the address of that name, so `LayoutConformanceShadowNode::Handle()` in YogaLayoutableShadowNode.cpp never matched the handle of the registered component, and strict and compatibility errata were never applied. Declare it `extern` and define it in a .cpp, like View and the other core components. Add a Fantom test for strict and compatibility mode, and register LayoutConformance in the Fantom tester. Co-Authored-By: Claude Opus 5.5 --- .../__tests__/LayoutConformance-itest.js | 72 +++++++++++++++++++ .../view/LayoutConformanceShadowNode.cpp | 15 ++++ .../view/LayoutConformanceShadowNode.h | 2 +- .../view/YogaLayoutableShadowNode.cpp | 4 +- .../src/stubs/StubComponentRegistryFactory.h | 2 + .../ReactAndroidFrameworksCxx.api | 2 +- .../api-snapshots/ReactAppleFrameworksCxx.api | 2 +- .../ReactCommonFrameworksCxx.api | 2 +- 8 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 packages/react-native/Libraries/Components/LayoutConformance/__tests__/LayoutConformance-itest.js create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.cpp diff --git a/packages/react-native/Libraries/Components/LayoutConformance/__tests__/LayoutConformance-itest.js b/packages/react-native/Libraries/Components/LayoutConformance/__tests__/LayoutConformance-itest.js new file mode 100644 index 00000000000..631c9a7c5b8 --- /dev/null +++ b/packages/react-native/Libraries/Components/LayoutConformance/__tests__/LayoutConformance-itest.js @@ -0,0 +1,72 @@ +/** + * 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. + * + * @flow strict-local + * @fantom_flags enableFlexboxAutoMinSizeInStrictMode:* + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; + +import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; +import {createRef} from 'react'; +import { + View, + experimental_LayoutConformance as LayoutConformance, +} from 'react-native'; + +type Mode = 'strict' | 'compatibility'; + +function renderRowWidth(mode: Mode, outerMode?: Mode): number { + const root = Fantom.createRoot(); + const rowRef = createRef(); + + const content = ( + + + {/* With YGErrataStretchFlexBasis this row grows to its parent's + width, even though its content takes no space. */} + + + + + + ); + + Fantom.runTask(() => { + root.render( + outerMode != null ? ( + {content} + ) : ( + content + ), + ); + }); + + const width = nullthrows(rowRef.current).getBoundingClientRect().width; + root.destroy(); + return width; +} + +test('strict mode lays out its subtree without the stretch flex-basis errata', () => { + expect(renderRowWidth('strict')).toBe(0); +}); + +// Compatibility is also the default, so check it under a strict subtree. +test('compatibility mode inside strict mode brings the stretch flex-basis errata back', () => { + expect(renderRowWidth('compatibility', 'strict')).toBe(60); +}); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.cpp new file mode 100644 index 00000000000..47e924ab972 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.cpp @@ -0,0 +1,15 @@ +/* + * 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 "LayoutConformanceShadowNode.h" + +namespace facebook::react { + +// NOLINTNEXTLINE(facebook-hte-CArray,modernize-avoid-c-arrays) +const char LayoutConformanceShadowNodeComponentName[] = "LayoutConformance"; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.h index 00bb8348536..8fbbbaf9bee 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/LayoutConformanceShadowNode.h @@ -15,7 +15,7 @@ namespace facebook::react { // NOLINTNEXTLINE(facebook-hte-CArray,modernize-avoid-c-arrays) -constexpr const char LayoutConformanceShadowNodeComponentName[] = "LayoutConformance"; +extern const char LayoutConformanceShadowNodeComponentName[]; class LayoutConformanceShadowNode final : public ConcreteShadowNode< LayoutConformanceShadowNodeComponentName, diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp index ce9951fe4a2..c7d04022fe2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -562,7 +562,9 @@ void YogaLayoutableShadowNode::configureYogaTree( YGErrata YogaLayoutableShadowNode::resolveErrata(YGErrata defaultErrata) const { // This runs for every child on every layout, and almost no node is a // LayoutConformance node, so a failing `dynamic_cast` would be the common - // case. The class is final, so comparing component handles is equivalent. + // case. The class is final and its component name is defined once, in + // LayoutConformanceShadowNode.cpp, so comparing component handles is + // equivalent. if (getComponentHandle() == LayoutConformanceShadowNode::Handle()) { const auto& layoutConformanceNode = static_cast(*this); diff --git a/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h b/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h index b68623060fa..9fc4f604856 100644 --- a/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h +++ b/private/react-native-fantom/tester/src/stubs/StubComponentRegistryFactory.h @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace facebook::react { @@ -27,6 +28,7 @@ inline ComponentRegistryFactory getDefaultComponentRegistryFactory() static auto providerRegistry = []() { auto providerRegistry = std::make_shared(); providerRegistry->add(concreteComponentDescriptorProvider()); + providerRegistry->add(concreteComponentDescriptorProvider()); providerRegistry->add(concreteComponentDescriptorProvider()); providerRegistry->add(concreteComponentDescriptorProvider()); providerRegistry->add(concreteComponentDescriptorProvider()); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index e793d6f7de2..26a081da785 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -376,6 +376,7 @@ struct facebook::hermes::ISetEventLoopControl : public facebook::jsi::ICast { const char facebook::react::AndroidTextInputComponentName[]; const char facebook::react::ImageComponentName[]; const char facebook::react::ImageManagerKey[]; +const char facebook::react::LayoutConformanceShadowNodeComponentName[]; const char facebook::react::ModalHostViewComponentName[]; const char facebook::react::ParagraphComponentName[]; const char facebook::react::RawTextComponentName[]; @@ -386,7 +387,6 @@ const char facebook::react::TextComponentName[]; const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; constexpr auto facebook::react::kSimpleThreadSafeCacheSizeCap; -constexpr char facebook::react::LayoutConformanceShadowNodeComponentName[]; constexpr char* const facebook::react::TextLayoutManagerKey; constexpr facebook::react::ReactNativeVersionType facebook::react::ReactNativeVersion; constexpr facebook::react::Tag facebook::react::kNoTag; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index b05f7665f7d..74c807232e9 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -3221,6 +3221,7 @@ struct facebook::hermes::ISetEventLoopControl : public facebook::jsi::ICast { const char facebook::react::ImageComponentName[]; const char facebook::react::ImageManagerKey[]; +const char facebook::react::LayoutConformanceShadowNodeComponentName[]; const char facebook::react::ModalHostViewComponentName[]; const char facebook::react::ParagraphComponentName[]; const char facebook::react::RawTextComponentName[]; @@ -3232,7 +3233,6 @@ const char facebook::react::TextInputComponentName[]; const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; constexpr auto facebook::react::kSimpleThreadSafeCacheSizeCap; -constexpr char facebook::react::LayoutConformanceShadowNodeComponentName[]; constexpr char* const facebook::react::TextLayoutManagerKey; constexpr facebook::react::ReactNativeVersionType facebook::react::ReactNativeVersion; constexpr facebook::react::Tag facebook::react::kNoTag; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 2772d8e63b9..3cf46ddabcb 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -73,6 +73,7 @@ struct facebook::hermes::ISetEventLoopControl : public facebook::jsi::ICast { const char facebook::react::ImageComponentName[]; const char facebook::react::ImageManagerKey[]; +const char facebook::react::LayoutConformanceShadowNodeComponentName[]; const char facebook::react::ModalHostViewComponentName[]; const char facebook::react::ParagraphComponentName[]; const char facebook::react::RawTextComponentName[]; @@ -83,7 +84,6 @@ const char facebook::react::TextComponentName[]; const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; constexpr auto facebook::react::kSimpleThreadSafeCacheSizeCap; -constexpr char facebook::react::LayoutConformanceShadowNodeComponentName[]; constexpr char* const facebook::react::TextLayoutManagerKey; constexpr facebook::react::ReactNativeVersionType facebook::react::ReactNativeVersion; constexpr facebook::react::Tag facebook::react::kNoTag;