Skip to content

Don't clone zeroed out display: none subtrees on every layout pass - #2055

Open
shubh73 wants to merge 1 commit into
react:mainfrom
shubh73:skip-recloning-zeroed-display-none
Open

shubh73 wants to merge 1 commit into
react:mainfrom
shubh73:skip-recloning-zeroed-display-none

Conversation

@shubh73

@shubh73 shubh73 commented Oct 31, 2026 •

Copy link
Copy Markdown

Summary

zeroOutLayoutRecursively runs for every display: none child on every layout pass of its parent, and calls cloneChildrenIfNeeded() at every level of the hidden subtree. When that subtree is shared with an earlier tree, the clone callback fires for every hidden node on every pass, even though zeroing it out again changes nothing.

In React Native each of those callbacks is a ShadowNode clone, and Fabric's layout() doesn't descend into display: none, so nothing settles them. Every commit that lays out the parent of a hidden view clones everything under it again. Apps that keep screens mounted with display: none, e.g. under <Activity mode="hidden">, pay this on every update for every hidden screen.

This skips cloning and recursing into children whose subtree is already zeroed out: hasNewLayout is set and the layout equals a zeroed out LayoutResults, all the way down. Children are only cloned once one of them needs zeroing out, e.g. after a node was added inside a hidden subtree. Layout results are unchanged.

configVersion is ignored in that comparison. It only validates the layout cache, which is empty after zeroing out, and embedders set it again when they give a cloned node a config. React Native does this for every clone, so without it the check would never match there.

Some notes for review:

  • LayoutResults::operator== skips the generation-tagged caches, computedAutoMinMainSize and the raw dimensions. That's fine here: laying out or measuring a node writes fields it does compare (cachedLayout, the cached measurements), so a node only matches if nothing has laid it out since it was zeroed out.
  • The fast path relies on the embedder leaving hasNewLayout set under display: none nodes, as React Native does. An embedder that clears it (relayout_clones_display_none_subtree_without_new_layout) gets the old behaviour.
  • Skipped children keep their owner from the earlier tree.
  • cloneChildrenIfNeeded still eagerly clones display: contents children of a cloned node, so those are cloned one level down. Anything deeper isn't.

React Native side: react/react-native#59706, which adds a Fantom test on top of this.

Changelog: [General][Fixed] - Fix display: none subtrees being cloned on every layout pass of their parent

Metrics

OPPO CPH2667 (Android 16), React Native RNTester release build, with one visible screen and N hidden display: none screens of about 500 nodes each. Each value is the extra time per update to the visible screen compared with N = 0, as the median of three alternating rounds of 500 updates. The range across rounds is in brackets.

stock this change
Resize, N = 3 +4.1 ms (3.6–5.0) +1.8 ms (1.4–3.3)
Resize, N = 5 +5.9 ms (5.7–7.1) +3.6 ms (3.0–4.1)
Resize, N = 10 +13.0 ms (11.4–14.2) +5.9 ms (5.7–6.4)
Colour change only, N = 10 +6.1 ms (4.6–6.1) +2.9 ms (2.8–3.1)

What's left is mostly in React Native, outside Yoga.

Test plan

Added six tests to tests/YGPersistentNodeCloningTest.cpp, using its clone callback harness:

  • relayout_does_not_clone_zeroed_out_display_none_subtree
  • adding_node_to_display_none_subtree_does_not_clone_zeroed_out_subtrees
  • relayout_does_not_clone_display_contents_in_display_none_subtree
  • relayout_does_not_clone_display_none_subtree_after_config_is_reapplied: clones part of a hidden subtree and calls YGNodeSetConfig on each clone, as React Native does.
  • showing_display_none_subtree_matches_never_hidden_layout
  • relayout_clones_display_none_subtree_without_new_layout

Without the change in CalculateLayout.cpp, the first four fail and the last two pass.

  • ./yogatests: 877 tests pass on main. With this change, all 883 pass. With only the tests, 879 pass and those 4 fail. I built with CXXFLAGS=-Wno-error=character-conversion, because googletest 1.12.1 doesn't build under -Werror with Apple clang 21 (gtest-printers.h).
  • yarn format-check-cpp and yarn lint pass.

🤖 Generated with Claude Code

@meta-cla meta-cla Bot added the CLA Signed label Oct 31, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 31, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shubh73
shubh73 force-pushed the skip-recloning-zeroed-display-none branch from 41c43d5 to 8f6d015 Compare October 31, 2026 02:10
shubh73 added a commit to shubh73/react-native that referenced this pull request Oct 31, 2026
Tests that a hidden subtree isn't cloned on every commit that lays out
its parent, through React commits and setNativeProps, and that hidden
screens lay out the same as never-hidden ones once shown again.

The second test depends on react/yoga#2055 and fails until that change
is synced into the vendored copy of Yoga.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamonholmgren

Copy link
Copy Markdown
Contributor

Probably related, although I don't think they're duplicates? react/react-native#59302

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants