From 629e0201de66232f566fc44627bfd157e929faae Mon Sep 17 00:00:00 2001 From: Open Source Bot Date: Tue, 3 Nov 2026 02:20:59 -0800 Subject: [PATCH] Don't clone zeroed out `display: none` subtrees on every layout pass (#2055) Summary: zeroOutLayoutRecursively runs for every display: none child on every layout pass of its parent, and called cloneChildrenIfNeeded() at every level of the hidden subtree. When the subtree is shared with an earlier tree, the clone callback fired for every hidden node on every pass, even though zeroing it out again changed nothing. In React Native each of those callbacks is a ShadowNode clone. Skip cloning and recursing into children whose subtree is already zeroed out. configVersion is ignored in that check: it only validates the layout cache, which is empty after zeroing out, and embedders such as React Native set it again when they give a cloned node its config. Reviewed By: javache Differential Revision: D129010903 --- .../yoga/yoga/algorithm/CalculateLayout.cpp | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp index 192a6af77da..32cd9c4efab 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp @@ -663,15 +663,50 @@ static bool measureNodeWithFixedSize( return false; } +static bool isLayoutZeroedOutRecursively(const yoga::Node* const node) { + if (!node->getHasNewLayout()) { + return false; + } + // `configVersion` only validates the layout cache, which is empty after + // zeroing out. Embedders set it again when they give a cloned node a config. + LayoutResults zeroedOutLayout; + zeroedOutLayout.setDimension(Dimension::Width, 0); + zeroedOutLayout.setDimension(Dimension::Height, 0); + zeroedOutLayout.configVersion = node->getLayout().configVersion; + if (!(node->getLayout() == zeroedOutLayout)) { + return false; + } + for (const auto child : node->getChildren()) { + if (!isLayoutZeroedOutRecursively(child)) { + return false; + } + } + return true; +} + void zeroOutLayoutRecursively(yoga::Node* const node) { node->getLayout() = {}; node->setLayoutDimension(0, Dimension::Width); node->setLayoutDimension(0, Dimension::Height); node->setHasNewLayout(true); - node->cloneChildrenIfNeeded(); - for (const auto child : node->getChildren()) { - zeroOutLayoutRecursively(child); + // A display: none subtree is zeroed out on every layout pass of its parent. + // Most of it is usually still zeroed out from an earlier pass, but owned by + // an earlier tree, so cloning it to zero it out again changes nothing. Only + // clone the children, and recurse, when part of the subtree needs it. + // + // This relies on the embedder leaving `hasNewLayout` set under display: none + // nodes. Children that are skipped keep their owner from the earlier tree. + bool didCloneChildren = false; + for (size_t i = 0; i < node->getChildCount(); i++) { + if (isLayoutZeroedOutRecursively(node->getChild(i))) { + continue; + } + if (!didCloneChildren) { + node->cloneChildrenIfNeeded(); + didCloneChildren = true; + } + zeroOutLayoutRecursively(node->getChild(i)); } }