Skip to content

Keep paragraph content and measured layouts across unchanged clones - #59063

Draft
tomekzaw wants to merge 1 commit into
react:mainfrom
tomekzaw:paragraph-keep-content-across-clones
Draft

tomekzaw wants to merge 1 commit into
react:mainfrom
tomekzaw:paragraph-keep-content-across-clones

Conversation

@tomekzaw

Copy link
Copy Markdown
Contributor

Summary:

When Yoga lays out a container again, it clones every child whose layout it touches, including unchanged siblings. A ParagraphShadowNode cloned that way (no new props, no new children) drops its memoized content_ (the attributed string) and measuredLayouts_.

Current behavior. In layout() the clone rebuilds its AttributedString and compares it with the one in its ParagraphState. After the paragraph's first commit the comparison fails even though nothing changed. Fragment::operator== compares parentShadowView.layoutMetrics, and the string stored in state captured those metrics before the paragraph had been laid out. So updateStateIfNeeded creates a new state for every sibling paragraph:

  • On Android each sibling then gets an UPDATE_STATE mount instruction. The UI thread rebuilds its Spannable, calls setText, lays the text out again and redraws it.
  • A trace of "mount 1000 labels, then change one" shows 1000 ReactTextViewManager.updateState, 1000 setText and 1000 onDraw calls for the one change.
  • With enablePreparedTextLayout, the clone also has no measured layout to reuse, so every sibling is laid out again on the JS thread.

With this change. A clone whose fragment has neither props nor children keeps content_ and measuredLayouts_. getContent drops both if the font-size multiplier or layout direction they were built for no longer matches. Unchanged siblings then produce no state update and no re-layout.

Why. Any update inside a container with many text siblings (non-virtualized lists, settings screens, forms, tables) currently costs UI-thread work for every sibling, and with prepared text layout also a JS-thread layout for every sibling.

Changelog:

[GENERAL] [FIXED] - Avoid state updates and re-layout of unchanged Text siblings when a sibling changes

Test Plan:

  • ./gradlew :packages:rn-tester:android:app:assembleDebug -PreactNativeArchitectures=arm64-v8a and yarn format-check-cpp pass on this branch.
  • Fantom could not be built locally: the bundled nlohmann/json.hpp fails with this machine's Apple clang. I'm relying on CI for it.
  • Screenshots were pixel-identical before and after a sequence of updates (all text, one item, colour, font size, container width), in four configurations with and without enablePreparedTextLayout.
  • iOS shares this code path but was not built or measured.

Benchmarks. RNTester release build with a text benchmark screen that is not part of this PR. Medians, main vs. this PR, variants interleaved.

Emulator (Pixel 10 Pro AVD, API 37), current main vs. this branch, 6 runs:

scenario metric main this PR change
1000 labels, change one UI-thread CPU 21.8 ms 5.2 ms −76%
time to pixels 58.6 ms 33.6 ms −43%
300 strings, change one UI-thread CPU 29.0 ms 6.0 ms −79%
time to pixels 49.3 ms 32.5 ms −34%
1000 labels, change one, enablePreparedTextLayout JS-thread CPU 30.4 ms 18.5 ms −39%
time to pixels 66.2 ms 49.1 ms −26%
mounts no significant change

Physical Samsung Galaxy A16, the same change on an earlier main (d4d0813969c), 8 runs. #59016 was applied in both arms.

scenario main this change change
1000 labels, change one right after mount: time to pixels 222 ms 78 ms −65%
300 strings, change one right after mount: time to pixels 164 ms 36 ms −78%
1000 labels, change one, prepared text: time to pixels 228 ms 73 ms −68%
same: JS-thread CPU 132 ms 44 ms −67%

🤖 Generated with Claude Code

A ParagraphShadowNode cloned with neither new props nor new children (for
example by Yoga because a sibling changed) dropped its memoized content and
measured layouts. layout() then rebuilt the attributed string, which no longer
compared equal to the one in state because the fragments' parentShadowView
layout metrics had changed, so every such sibling got a state update and was
re-set on the UI thread. With prepared text layout the sibling was also laid
out again.

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 14, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner labels Oct 14, 2026

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant