Repository navigation
Defer cloning unchanged Yoga children until layout - #59302
Open
hryhoriiK97 wants to merge 4 commits into
Open
hryhoriiK97 wants to merge 4 commits into
hryhoriiK97 wants to merge 4 commits into
Conversation
When a node gets a new list of children, `YogaLayoutableShadowNode::adoptYogaChild` clones every child that a previous revision of the node still owns, so that the new node owns the Yoga node of each child. Yoga already clones a child it doesn't own before it lays the child out, through `yogaNodeCloneCallbackConnector`, so the eager clone is only needed when the node is laid out. Behind the new `deferYogaChildCloningUntilLayout` feature flag (default `false`), such children stay shared with the previous revision, and Yoga clones the ones it lays out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yoga can set a new layout on a node from its cache without cloning the node's children, so with `deferYogaChildCloningUntilLayout` a child that is still shared with another revision could be written to by `YogaLayoutableShadowNode::layout`. Clone such a child first. Add Fantom tests for a node restored from Yoga's cache, an absolute child of a static node, `display: contents` and baseline-aligned text, each compared with a fresh render of the same tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerate the feature flag files with `yarn featureflags`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerate the feature flag files with `yarn featureflags`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
When a node gets a new list of children,
YogaLayoutableShadowNode::adoptYogaChild(ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp) makes the node own the Yoga node of each child. A child that a previous revision of the node still owns, which is every unchanged child, is cloned withclone({})right away.Current behavior. A commit clones the path from the changed node to the root, and React clones the parent of every changed node. Each of those clones gets a new list of children, so every unchanged sibling along the path is cloned too, whether or not anything is laid out. While a list scrolls, the ScrollView writes its content offset into its state on every frame, so every frame clones every sibling of every ancestor of the ScrollView. A color change on one row of a list clones all the other rows. Most of these clones are never laid out: Yoga only needs to own a child when it lays the child out, and it already clones a child it doesn't own at that point, through
yogaNodeCloneCallbackConnector, the same way it does today for children of a node cloned without new children.With this change. Behind the new
deferYogaChildCloningUntilLayoutfeature flag (defaultfalse),adoptYogaChildleaves a child owned by another node shared. Yoga clones every child it doesn't own when it lays out or measures the parent, unless it restores the parent from its layout cache. Yoga can mark such a cached parent as having a new layout without cloning its children, soYogaLayoutableShadowNode::layoutnow clones a shared child that carries a new layout before writing into it. A child owned by the node itself is still cloned, so the existing guard against a reused Yoga node address keeps working.Two side effects. Unchanged children are now the same
ShadowNodein both revisions, which lets the differ skip more of them withskipUnchangedChildrenInDifferentiator(#59242). And unchanged measurable siblings are no longer dirtied:clone({})runscompleteCloneandadopt, which dirty a TextInput, Switch or Android ProgressBar (the defaultshouldNewRevisionDirtyMeasurementreturnstrue) and always dirty an Android TextInput (AndroidTextInputComponentDescriptor::adoptcallsdirtyLayout). So a change next to one of them laid out their parent again.Changelog:
[Internal]
Test Plan:
yarn featureflags --verify-unchangedpasses. The pinnedclang-format(21.1.2) passes on the changed C++ file, andprettier,eslintandflow focus-checkpass on the new test file.View-deferYogaChildCloning-itest.jsruns with@fantom_flags deferYogaChildCloningUntilLayout:*and checks the mutations and layouts for: a color change on one of 50 children (oneUpdatemutation), a child that grows after several commits that left its siblings shared, a nested child whose parent was shared, scrolling a ScrollView and then changing its content, hiding and showing a node whose children were shared, and the steps ofView-yogaNodeOwnerAssertion-itest.js. Four more tests reach their last state through commits that leave children shared and compare every layout with a fresh render of that state: a node restored from Yoga's layout cache, an absolute child of aposition: 'static'node,display: contentsand baseline-aligned text.yarn fantom View-deferYogaChildCloning-itest: 20 passed (10 tests × flag off and on), andView-yogaNodeOwnerAssertion-iteststill passes. The full suite (yarn fantom) passes with the flag's default switched totruelocally, twice (3,346 passed, 110 skipped), on the Debug tester wherereact_native_assertis on, including the ownership check inYogaLayoutableShadowNode::layout. In one earlier run with the flag on, the tester crashed (SIGSEGV) in three files, and in a run with the flag off it crashed in a fourth; none of the three crashed again in 40 runs each, 8 at a time, with the flag on or off.layout()guard. With Yoga's change that stopsdisplay: nonechildren from keeping a stalehasNewLayoutreverted locally, the steps ofView-yogaNodeOwnerAssertion-itest.jswith the flag on trip the ownership assertion without the guard and pass with it.xcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1). The two arms are the same binary with the flag switched by a launch argument, in the order before, after, after, before. Scrolling was driven by Argent on the physical iPhone. Timed: commit and diff time of each transaction fromTransactionTelemetry, with a local counter, everyShadowNodeclone made throughConcreteComponentDescriptor::cloneShadowNode, and the JS thread's CPU time.ShadowNodeclones per launchadoptYogaChild/ clones requested by Yoga during layoutAbout 102,000 of the 121,000 clones made in
adoptYogaChildwere never laid out. The commit time of the larger transactions grows because the clones that are needed now happen during layout, inside the commit, while the eager clones they replace happened when React built the new tree, before the commit started. The JS thread's total CPU time per launch, which covers both, is lower. The last two rows come from a later run of the same scenario with the code in this PR, read withthread_info.With
skipUnchangedChildrenInDifferentiator(#59242) on in both arms:skipUnchangedChildrenInDifferentiatoron in both arms🤖 Generated with Claude Code