Skip to content

Re-land "Keep paragraph content and measured layouts across unchanged clones" - #59285

Open
tomekzaw wants to merge 3 commits into
react:mainfrom
tomekzaw:reland-paragraph-keep-content-across-clones
Open

tomekzaw wants to merge 3 commits into
react:mainfrom
tomekzaw:reland-paragraph-keep-content-across-clones

Conversation

@tomekzaw

@tomekzaw tomekzaw commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

Re-land of #59063, which was reverted in ad2f451 because it broke textAdjustsDynamicLayout on Android. This PR has three commits: the Android fix for that regression, #59063 unchanged, and fixes for the two prepared-layout cache concerns raised in the revert.

#59063 (unchanged). A ParagraphShadowNode cloned without new props or children (e.g. by Yoga, because a sibling changed) keeps its attributed string and measured layouts. Unchanged siblings then get no state update and no re-layout. On a Galaxy A16, changing one of 1000 labels takes 66% less time to pixels than on current main (benchmarks at the end).

1. Why textAdjustsDynamicLayout broke, and the fix (ReactTextView). ReactTextView fits adjustsFontSizeToFit text by shrinking the font size spans of its Spannable in place. When the view grows, onSizeChanged schedules another fit, but that fit starts from the largest span it finds, which is the already-shrunk size. The text can shrink further but never grow back. It only grew back when a state update happened to deliver a new Spannable with the original sizes. #59063 removed the spurious state update the example relied on, so the text stayed small after the container grew.

This is also a bug on main today, just one that depends on order: in RNTester, Set Height to 60 → 20 → 40 right after opening the example leaves the text small at 40, because no state update lands on that last resize.

With this change, the view keeps the font size spans the text had before the first fit and restores them, along with the paint's base text size, before each new fit.

2. Carried content is validated before its layouts are used. measureContent looked up a carried layout before getContent checked that the carried content was built for the current font size multiplier and layout direction. With enablePreparedTextLayout, a font scale change while the app is running reused layouts measured at the old scale, so text was drawn at the new size into boxes measured at the old one and was cut off. The check now runs first.

3. Equal measured sizes don't imply equal layouts. findUsableLayout matches a layout by its measured size. Text with adjustsFontSizeToFit can measure the same under different constraints with a different font size, so its prepared layouts are no longer carried across revisions. When several layouts of the same size exist, the one measured for the current revision is preferred over carried ones.

Changelog:

[GENERAL] [FIXED] - Avoid state updates and re-layout of unchanged Text siblings when a sibling changes
[ANDROID] [FIXED] - adjustsFontSizeToFit text grows back when its container grows

Test Plan:

  • ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.views.text.*": 94 tests pass. The new adjustsFontSizeToFitGrowsBackWhenContainerHeightGrows fails without the ReactTextView change (font size stays at 4 instead of 40).
  • yarn format-check-java, format-check-kotlin and format-check-cpp pass on the changed files.
  • RNTester textAdjustsDynamicLayout, release build on an Android emulator:
steps main #59063 alone this PR
open → 40 grows stays small grows
open → 60 → 20 → 40 stays small stays small grows
  • Live changes compared pixel by pixel with a fresh remount of the same state, using a local RNTester screen (not part of this PR) with an adjustsFontSizeToFit text, mixed LTR/RTL texts, nested spans and a counter. Release builds; Android on a Galaxy A16, iOS on an iPhone Air simulator (iOS 26.5). ✓ = identical to the remount.
live change Android default Android enablePreparedTextLayout iOS
container height 20 → 40 → 60 → 20 → 40 (adjustsFontSizeToFit) ✓ (#59063 alone: ✗) ✓ ✓
direction LTR → RTL → LTR on the parent ✓ ✓ ✓
update one sibling's text ✓ ✓ ✓
font scale 1.0 → 1.3 → 1.0 / Dynamic Type L → XXL → L ✓ ✓ (#59063 alone: ✗, text clipped) ✓
density 450 → 360 → 450 dpi, fixTextLayoutOnPixelDensityChange on ✓ ✓ n/a
  • Text with inline views (Text examples → Inline views, including the RTL ones), both Android text paths: four live font scale changes, no crash, text reflows correctly.
  • Not run: Fantom (the bundled nlohmann/json.hpp doesn't build with this machine's Apple clang, so I'm relying on CI), and a physical iPhone.

Benchmarks. Samsung Galaxy A16, RNTester release builds of current main (c7429b0) and this PR, with a text benchmark screen that is not part of this PR. The two builds alternated round by round; medians of 12 runs (4 rounds × 3 runs after a warm-up run), battery 27.6–27.9 °C.

changing one item, right after mount main this PR change
1000 labels: time to pixels 225 ms 77 ms −66%
1000 labels: UI-thread CPU 178 ms 38 ms −79%
1000 labels, enablePreparedTextLayout: time to pixels 250 ms 74 ms −70%
1000 labels, enablePreparedTextLayout: JS-thread CPU 153 ms 41 ms −74%
300 strings: time to pixels 149 ms 36 ms −76%
300 strings: UI-thread CPU 139 ms 35 ms −75%
100-post feed: time to pixels 95 ms 88 ms −7%

Mounts: time to pixels is unchanged (−2% to +4%, within run-to-run noise). With enablePreparedTextLayout, mounting 1000 labels uses about 10 ms more JS-thread CPU and about 40 ms more total CPU (+9%) in every round, probably from copying the prepared layouts into clones, without a change in time to pixels.

Two earlier rounds were left out: in them both builds produced many extra frames per update, probably because Android Studio's screen mirroring was attached to the phone at the time. This PR was still faster in those rounds (1000 labels: 164 ms vs 2406 ms).

🤖 Generated with Claude Code

tomekzaw and others added 3 commits October 21, 2026 07:19
…the view resizes

ReactTextView fits the font size by shrinking the font size spans of its Spannable in
place. When the view's size changes, onSizeChanged schedules another fit, but that fit
starts from the largest span it finds, which is the already-shrunk size, so the text
can shrink further but never grow back. It only grew back when a state update
happened to deliver a new Spannable with the original sizes.

Keep the size spans the text had before the first fit, and restore them (and the
paint's base text size) before fitting again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-land of react#59063 (reverted in ad2f451). A ParagraphShadowNode cloned
without new props or children (e.g. by Yoga, because a sibling changed) keeps
its attributed string and measured layouts instead of rebuilding them, so
unchanged siblings no longer get a state update and a re-layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- measureContent looked up a carried layout before getContent checked that the
  carried content was built for the current font size multiplier and layout
  direction. Check (and drop) it first.
- Don't carry prepared layouts of text with adjustsFontSizeToFit: its layout
  depends on the constraints, not only on the measured size that
  findUsableLayout matches on.
- Prefer layouts measured for the current revision over carried ones of the
  same size.

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

meta-codesync Bot commented Oct 21, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D126622505.

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