From 7eee607f952217b46c1c0d717bd3c1509f679707 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 15 Oct 2026 12:50:00 +0300 Subject: [PATCH] Only dirty Yoga nodes in setSize/setPositionType when the value changes Component descriptors call these from adopt(), which runs on every clone. Dirtying unconditionally made every commit under such a node (e.g. a ModalHostView) re-run layout and pixel-grid rounding over the whole surface. Co-Authored-By: Claude Opus 5.5 --- .../view/YogaLayoutableShadowNode.cpp | 21 +++++-- .../components/view/tests/ViewTest.cpp | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 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 fb7c6e4ffe87..f7085e2a0795 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -596,11 +596,20 @@ YogaLayoutableShadowNode& YogaLayoutableShadowNode::cloneChildInPlace( void YogaLayoutableShadowNode::setSize(Size size) const { ensureUnsealed(); + auto width = yoga::StyleSizeLength::points(size.width); + auto height = yoga::StyleSizeLength::points(size.height); + + // Component descriptors call this from `adopt`, which runs on every clone. + // Dirtying the node when nothing changed would make every commit that clones + // it re-run layout over the whole tree. auto style = yogaNode_.style(); - style.setDimension( - yoga::Dimension::Width, yoga::StyleSizeLength::points(size.width)); - style.setDimension( - yoga::Dimension::Height, yoga::StyleSizeLength::points(size.height)); + if (width == style.dimension(yoga::Dimension::Width) && + height == style.dimension(yoga::Dimension::Height)) { + return; + } + + style.setDimension(yoga::Dimension::Width, width); + style.setDimension(yoga::Dimension::Height, height); yogaNode_.setStyle(style); yogaNode_.setDirty(true); } @@ -635,6 +644,10 @@ void YogaLayoutableShadowNode::setPositionType( ensureUnsealed(); auto style = yogaNode_.style(); + if (style.positionType() == yoga::scopedEnum(positionType)) { + return; + } + style.setPositionType(yoga::scopedEnum(positionType)); yogaNode_.setStyle(style); yogaNode_.setDirty(true); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp index ee71635f315c..14ff9ea528d2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp @@ -326,6 +326,63 @@ TEST_F(YogaDirtyFlagTest, showingHiddenNodeMustDirtyItAndLayOutItsChildren) { EXPECT_EQ(child->getLayoutMetrics().frame.size.width, 42); } +TEST_F(YogaDirtyFlagTest, settingUnchangedSizeMustNotDirtyYogaNode) { + /* + * Component descriptors call `setSize` from `adopt`, which runs on every + * clone. Setting the size a node already has must *not* dirty its Yoga node, + * otherwise every commit that clones it re-runs layout. + */ + auto setSize = [](const ShadowNode& oldShadowNode) { + auto newShadowNode = oldShadowNode.clone({}); + static_cast(*newShadowNode) + .setSize({.width = 100, .height = 200}); + return newShadowNode; + }; + + auto sizedRootShadowNode = + rootShadowNode_->cloneTree(innerShadowNode_->getFamily(), setSize); + + /* + * Changing the size *must* dirty the Yoga node. + */ + EXPECT_TRUE( + static_cast(*sizedRootShadowNode).layoutIfNeeded()); + + auto newRootShadowNode = + sizedRootShadowNode->cloneTree(innerShadowNode_->getFamily(), setSize); + + EXPECT_FALSE( + static_cast(*newRootShadowNode).layoutIfNeeded()); +} + +TEST_F(YogaDirtyFlagTest, settingUnchangedPositionTypeMustNotDirtyYogaNode) { + /* + * Setting the position type a node already has must *not* dirty its Yoga + * node. + */ + auto setPositionType = [](const ShadowNode& oldShadowNode) { + auto newShadowNode = oldShadowNode.clone({}); + static_cast(*newShadowNode) + .setPositionType(YGPositionTypeAbsolute); + return newShadowNode; + }; + + auto absoluteRootShadowNode = rootShadowNode_->cloneTree( + innerShadowNode_->getFamily(), setPositionType); + + /* + * Changing the position type *must* dirty the Yoga node. + */ + EXPECT_TRUE( + static_cast(*absoluteRootShadowNode).layoutIfNeeded()); + + auto newRootShadowNode = absoluteRootShadowNode->cloneTree( + innerShadowNode_->getFamily(), setPositionType); + + EXPECT_FALSE( + static_cast(*newRootShadowNode).layoutIfNeeded()); +} + TEST_F(YogaDirtyFlagTest, clonedPropsPreserveAspectRatio) { ContextContainer contextContainer{}; PropsParserContext parserContext{-1, contextContainer};