Skip to content

Commit ddf6a63

Browse files
mozziusclaude
andcommitted
Don't dirty display: none nodes because of their children
A display: none node is laid out with zero size whatever its children are, and Yoga never cleans the descendants of a hidden node. updateYogaChildren dirtied a hidden node whenever it was cloned with a dirty child, so a child created or changed while the node was hidden made every later commit under it re-run layout from the root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 210b1fb commit ddf6a63

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

‎packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,17 @@ void YogaLayoutableShadowNode::updateYogaChildren() {
357357
bool isClean = !YGNodeIsDirty(&yogaNode_) &&
358358
getChildren().size() == YGNodeGetChildCount(&yogaNode_);
359359

360-
auto oldYogaChildren =
361-
isClean ? yogaNode_.getChildren() : std::vector<yoga::Node*>{};
360+
// A `display: none` node is laid out with zero size whatever its children
361+
// are, so its children can't make its layout stale. Yoga never cleans the
362+
// descendants of a hidden node either, so a child that was created or
363+
// changed while the node was hidden stays dirty until the node is shown.
364+
// Comparing children here would dirty the hidden node, and every ancestor
365+
// up to the root, on every commit that clones it. Showing the node again
366+
// changes its style, which dirties it in `updateYogaProps`.
367+
bool isHidden = yogaNode_.style().display() == yoga::Display::None;
368+
369+
auto oldYogaChildren = isClean && !isHidden ? yogaNode_.getChildren()
370+
: std::vector<yoga::Node*>{};
362371

363372
yogaNode_.setChildren({});
364373
yogaLayoutableChildren_.clear();
@@ -371,7 +380,7 @@ void YogaLayoutableShadowNode::updateYogaChildren() {
371380
appendYogaChild(yogaLayoutableChild);
372381
adoptYogaChild(i);
373382

374-
if (isClean) {
383+
if (isClean && !isHidden) {
375384
auto yogaChildIndex = yogaLayoutableChildren_.size() - 1;
376385
auto& oldYogaChildNode = *oldYogaChildren.at(yogaChildIndex);
377386
auto& newYogaChildNode =

‎packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewTest.cpp‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,100 @@ TEST_F(YogaDirtyFlagTest, updatingStateForScrollViewMistNotDirtyYogaNode) {
232232
static_cast<RootShadowNode&>(*newRootShadowNode).layoutIfNeeded());
233233
}
234234

235+
TEST_F(YogaDirtyFlagTest, dirtyChildOfHiddenNodeMustNotDirtyIt) {
236+
/*
237+
* A `display: none` node is laid out with zero size whatever its children
238+
* are, and Yoga never cleans the descendants of a hidden node. A child that
239+
* changed while its parent was hidden must *not* dirty the parent, otherwise
240+
* every commit that clones the parent re-runs layout from the root.
241+
*/
242+
auto& childFamily = innerShadowNode_->getChildren().at(0)->getFamily();
243+
244+
auto hiddenRootShadowNode = rootShadowNode_->cloneTree(
245+
innerShadowNode_->getFamily(), [](const ShadowNode& oldShadowNode) {
246+
auto viewProps = std::make_shared<ViewProps>();
247+
viewProps->yogaStyle.setDisplay(yoga::Display::None);
248+
return oldShadowNode.clone(ShadowNodeFragment{.props = viewProps});
249+
});
250+
251+
/*
252+
* Hiding the node *must* dirty it.
253+
*/
254+
EXPECT_TRUE(
255+
static_cast<RootShadowNode&>(*hiddenRootShadowNode).layoutIfNeeded());
256+
257+
/*
258+
* Changing the layout of a child of the hidden node dirties the child, but
259+
* not the hidden node.
260+
*/
261+
auto changedRootShadowNode = hiddenRootShadowNode->cloneTree(
262+
childFamily, [](const ShadowNode& oldShadowNode) {
263+
auto viewProps = std::make_shared<ViewProps>();
264+
viewProps->yogaStyle.setDimension(
265+
yoga::Dimension::Width, yoga::StyleSizeLength::points(42));
266+
return oldShadowNode.clone(ShadowNodeFragment{.props = viewProps});
267+
});
268+
269+
EXPECT_FALSE(
270+
static_cast<RootShadowNode&>(*changedRootShadowNode).layoutIfNeeded());
271+
272+
/*
273+
* The child is still dirty, because the layout pass didn't visit it. A
274+
* later change to its non-layout props must not dirty the hidden node
275+
* either.
276+
*/
277+
auto newRootShadowNode = changedRootShadowNode->cloneTree(
278+
childFamily, [](const ShadowNode& oldShadowNode) {
279+
auto viewProps = std::make_shared<ViewProps>();
280+
viewProps->yogaStyle.setDimension(
281+
yoga::Dimension::Width, yoga::StyleSizeLength::points(42));
282+
viewProps->opacity = 0.5;
283+
return oldShadowNode.clone(ShadowNodeFragment{.props = viewProps});
284+
});
285+
286+
EXPECT_FALSE(
287+
static_cast<RootShadowNode&>(*newRootShadowNode).layoutIfNeeded());
288+
}
289+
290+
TEST_F(YogaDirtyFlagTest, showingHiddenNodeMustDirtyItAndLayOutItsChildren) {
291+
/*
292+
* Showing a hidden node again *must* dirty it, and the children that
293+
* changed while it was hidden must be laid out.
294+
*/
295+
auto& childFamily = innerShadowNode_->getChildren().at(0)->getFamily();
296+
297+
auto hiddenRootShadowNode = rootShadowNode_->cloneTree(
298+
innerShadowNode_->getFamily(), [](const ShadowNode& oldShadowNode) {
299+
auto viewProps = std::make_shared<ViewProps>();
300+
viewProps->yogaStyle.setDisplay(yoga::Display::None);
301+
return oldShadowNode.clone(ShadowNodeFragment{.props = viewProps});
302+
});
303+
static_cast<RootShadowNode&>(*hiddenRootShadowNode).layoutIfNeeded();
304+
305+
auto changedRootShadowNode = hiddenRootShadowNode->cloneTree(
306+
childFamily, [](const ShadowNode& oldShadowNode) {
307+
auto viewProps = std::make_shared<ViewProps>();
308+
viewProps->yogaStyle.setDimension(
309+
yoga::Dimension::Width, yoga::StyleSizeLength::points(42));
310+
return oldShadowNode.clone(ShadowNodeFragment{.props = viewProps});
311+
});
312+
static_cast<RootShadowNode&>(*changedRootShadowNode).layoutIfNeeded();
313+
314+
auto shownRootShadowNode = changedRootShadowNode->cloneTree(
315+
innerShadowNode_->getFamily(), [](const ShadowNode& oldShadowNode) {
316+
return oldShadowNode.clone(
317+
ShadowNodeFragment{.props = std::make_shared<ViewProps>()});
318+
});
319+
320+
EXPECT_TRUE(
321+
static_cast<RootShadowNode&>(*shownRootShadowNode).layoutIfNeeded());
322+
323+
auto child = std::static_pointer_cast<const ViewShadowNode>(
324+
shownRootShadowNode->getChildren().at(1)->getChildren().at(0));
325+
EXPECT_EQ(child->getLayoutMetrics().displayType, DisplayType::Flex);
326+
EXPECT_EQ(child->getLayoutMetrics().frame.size.width, 42);
327+
}
328+
235329
TEST_F(YogaDirtyFlagTest, clonedPropsPreserveAspectRatio) {
236330
ContextContainer contextContainer{};
237331
PropsParserContext parserContext{-1, contextContainer};

0 commit comments

Comments
 (0)