Repository navigation
Skip unchanged children when slicing in the differ - #59242
Open
hryhoriiK97 wants to merge 1 commit into
Open
hryhoriiK97 wants to merge 1 commit into
hryhoriiK97 wants to merge 1 commit into
Conversation
When a node changes, the differ slices the children of its old and new revision and compares them. Most of them are the same ShadowNode in both revisions, since a commit clones only the path to the root, but each one is still turned into a ShadowViewNodePair in both lists before the comparison skips it. Behind the new skipUnchangedChildrenInDifferentiator flag, the children of a matched pair are sliced side by side, leaving out each child that is the same node in both revisions at the same offset. Such a child produces no mutation, so the mutations are unchanged. The side-by-side slicing falls back to slicing both revisions in full when the child count, tags, flattening, position type, order index, visibility or ChildrenFormStackingContext differ, or when view culling is enabled. The per-child logic of the existing slicer moves into helpers shared by both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
@javache This touches the differ, so I'd value your view. Is skipping unchanged children like this a direction you'd accept? If so, would you prefer it split into the slicer refactor and the change behind the flag, or reviewed as one? |
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D126471123. |
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 changes, the differ (
calculateShadowViewMutations,ReactCommon/react/renderer/mounting/Differentiator.cpp) slices the children of its old and new revision withsliceChildShadowNodeViewPairsand compares the two lists. A commit clones only the path from the changed node to the root, so most of those children are the sameShadowNodein both revisions.Current behavior. Slicing builds a
ShadowViewNodePair, including aShadowView, for every child in both lists, recursing into flattened children, before the comparison finds that a child is unchanged and skips it. While a list scrolls, the ScrollView writes its content offset into its state on every frame, so every frame produces a commit that changes one node deep in the tree, and the differ slices every sibling along the path to it, twice. In the RNTester chat scenario below, 36 of the 56 child pairs compared in such a diff are the same node in both revisions.With this change. Behind the new
skipUnchangedChildrenInDifferentiatorfeature flag (defaultfalse), the differ slices the children of a matched pair side by side with the newsliceChangedChildShadowNodeViewPairsand leaves out each child that is the same node in both revisions at the same offset. Such a child and its flattened descendants give equal pairs in both lists, which the differ matches without a mutation, so the mutations computed from the shorter lists are the same. A child is kept when its offset differs, because a flattened ancestor moved and its frame has to be updated. The side-by-side slicing gives up, and the differ slices both revisions in full as before, when the revisions differ in a way that could change the order or the kind of the pairs: child count, tags, flattening, position type, order index (zIndex), visibility orChildrenFormStackingContext, or when view culling is enabled.The per-child logic of the existing slicer moves into helpers that both slicers share, so they can't drift apart.
Changelog:
[Internal]
Test Plan:
clang-format --dry-run --Werroron the changed C++ files passes.yarn featureflagsregenerated the accessors.Mounting-itest.jsnow runs with@fantom_flags skipUnchangedChildrenInDifferentiator:*, so every existing differ test also runs with the flag on, and it adds tests for an unchanged child next to a changed one, an unchanged child of a flattened view that moves, a scrolled ScrollView with unchanged siblings, a child added next to unchanged children and a child whosezIndexchanges.yarn fantom Mounting-itest: 56 passed (14 tests × 4 flag combinations). The full suite (FANTOM_FORCE_CI_MODE=1 yarn fantom) passes with the flag's default switched totruelocally, so that every test runs with it on (3,325 passed, 108 skipped), and with it off (same counts), twice each. In a first run with the flag on, the tester crashed once insetUpDefaultReactNativeEnvironment-Globals-ResizeObserver-itest.js; it did not crash again in three runs of that file alone or in two more full runs.removeClippedSubviews,adjustsFontSizeToFittext updates, scrolling a chat FlatList, typing in a TextInput, FlatList and SectionList scrolling.useTraitHiddenOnAndroid) are mirrored from the existing slicer.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: the diff phase of each mount transaction fromTransactionTelemetry, on the main thread.A mount gains nothing, since every node is new, and giving up on the side-by-side slicing costs nothing measurable. When every card moves, no card can be left out, but the text inside each card keeps its node and its offset relative to the card, so it is still left out.
🤖 Generated with Claude Code