Skip to content

Keep skipping unchanged children in the differ when a Text's string changes - #59373

Open
hryhoriiK97 wants to merge 2 commits into
react:mainfrom
hryhoriiK97:differ-skip-replaced-leaves
Open

hryhoriiK97 wants to merge 2 commits into
react:mainfrom
hryhoriiK97:differ-skip-replaced-leaves

Conversation

@hryhoriiK97

Copy link
Copy Markdown
Contributor

Summary:

Follow-up to #59242, behind the same skipUnchangedChildrenInDifferentiator feature flag (default false).

Current behavior. The side-by-side slicing from #59242 gives up, and the differ slices both revisions in full, when a child's tag differs between them. React gives a Text a new raw text node, with a new tag, every time its string changes. On Android, ParagraphShadowNode unsets FormsStackingContext, so a Paragraph's children are sliced together with the children of its parent. When a text inside a list changes, the slicing reaches the new raw text node while it slices the list's container, and gives up for the whole container. In the RNTester screen below (a ScrollView with 200 rows, where 20 values change every 100 ms and a clock text changes every 16 ms), every commit fell back to slicing all ~1,400 pairs, so the flag made no difference on Android.

With this change. When the tags (or the order indexes) differ, but both children are leaves that don't form a view, both are added to their lists as they are and the slicing continues. Such a node has no mount index, so it doesn't shift the views around it, and slicing in full lists it the same way. A replaced child that has children or forms a view still makes the slicing give up.

On iOS, #59242 already gives the full gain on this screen (~730 → ~193 µs per diff), and this change doesn't add to it. There a Paragraph forms a stacking context, so its raw text is reached only when the differ diffs the Paragraph itself, which has a single child.

Changelog:

[Internal]

Test Plan:

  • clang-format reports no changes for the C++ file. Prettier and ESLint pass for the test file, and yarn flow-check reports no errors in it.
  • Fantom. Two new tests in Mounting-itest.js: a Text whose string changes next to unchanged children (only the Paragraph is updated), and an empty View remounted with a new key next to unchanged children (no mutations). A temporary log confirmed that both reach the new branch with the flag on. yarn fantom Mounting-itest: 64 passed (16 tests × 4 flag combinations). The full suite (FANTOM_FORCE_CI_MODE=1 yarn fantom) passes with the flag off, and with its default switched to true locally so that every test runs with it on: 3,337 passed and 108 skipped both times.
  • Same mutations on device. With a local checker that computed every diff twice, once with the flag off and once with the flag and this change on, and compared the full mutation lists (type, parent, index and both shadow views), 5,400 commits on a Pixel 9a gave identical lists: 2,400 on the Live screen below (the new branch ran in 2,388 of them), 1,800 on the feed below, and 1,200 in RNTester's Text examples. Short runs of the FlatList and SectionList examples showed no difference either.
  • Measurement on Android. RNTester built with Gradle in Release (assembleRelease, arm64-v8a) on a Pixel 9a (Android 16), compiled with cmd package compile -m speed. The three arms are the same binary with a local switch (a system property read in C++, not part of this PR), launched in the order flag off, Skip unchanged children when slicing in the differ #59242, this PR, this PR, Skip unchanged children when slicing in the differ #59242, flag off, flag off, Skip unchanged children when slicing in the differ #59242, this PR. Each launch records a 30 s Perfetto trace with the C++ trace sections from Emit C++ TraceSection markers through ATrace on Android #59065.
RNTester Playground, 200 rows in a ScrollView, 20 values changed every 100 ms and a clock text every 16 ms (~1,250 commits per launch, 3 launches per arm) flag off flag on (#59242) flag on + this PR change vs. #59242
time in calculateShadowViewMutations 3,905 ms 3,858 ms 508 ms −87%
time in ShadowTree::commit 7,637 ms 7,586 ms 4,349 ms −43%
JS thread work (RuntimeScheduler::runEventLoop) 14,508 ms 14,455 ms 11,634 ms −20%
RNTester Playground, a 1,000-row feed that scrolls itself, time in calculateShadowViewMutations flag off flag on (#59242) flag on + this PR
launches with ~205,000 Yoga clones 711 ms (1 launch) 525 ms (2) 540 ms (2)
launches with ~250,000 Yoga clones 827 ms (2) 634 ms (1) 618 ms (1)

The feed renders a larger window of rows in some launches, so launches are compared with the same amount of work. The new branch ran once in the 1,800 feed commits checked above, so the ±3% between #59242 and this PR is noise; an earlier round gave 539 and 544 ms.

  • Measurement on iOS. The same Playground screen in RNTester built with xcodebuild in Release on a physical iPhone 15 Pro, with the three arms switched by launch environment, timing every call of calculateShadowViewMutations (2,100 diffs per launch): flag off ~730 µs per diff (3 launches), Skip unchanged children when slicing in the differ #59242 ~193 µs (3), this PR ~190 µs (6). The flag already works on iOS, and this PR doesn't change it.
  • Not run: other Android devices, and the C++ unit tests outside Fantom.

🤖 Generated with Claude Code

With skipUnchangedChildrenInDifferentiator on, the side-by-side slicing gives
up when a child's tag changes. React gives a Text a new raw text node whenever
its string changes, and on Android a Paragraph doesn't form a stacking context,
so the differ reaches that node while slicing the Paragraph's parent and slices
all of the parent's children in full. A replaced leaf that doesn't form a view
has no mount index, so both nodes can be listed as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 22, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 22, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hryhoriiK97

Copy link
Copy Markdown
Contributor Author

@javache A small follow-up to #59242. On Android the flag didn't help when a <Text> changes its string, so I fixed that case. On a Pixel 9a the differ is 87% faster on a screen with updating text, with the same mutations. iOS is not affected.

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 This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Software Mansion Partner: Software Mansion Partner 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.

1 participant