Skip to content

Refresh padding and text after a pixel density change (#59049) - #59049

Closed
brunocramos wants to merge 1 commit into
react:mainfrom
brunocramos:export-D124076552
Closed

brunocramos wants to merge 1 commit into
react:mainfrom
brunocramos:export-D124076552

Conversation

@brunocramos

@brunocramos brunocramos commented Oct 13, 2026 •

Copy link
Copy Markdown

Summary:

When an Android app handles density changes itself (density in android:configChanges), its surface is laid out again at the new density, but some mounted views keep sizes from the old one:

  • Padding and overflow insets are sent to views in pixels, but only when their dp values change, which a density change usually doesn't do. A TextInput keeps its old pixel padding, so its text and placeholder can be clipped.
  • Paragraph state only updates when the text, its attributes or its measured size change, so Text keeps the layout it built at the old density and draws at the old size.

With the new fixTextLayoutOnPixelDensityChange feature flag on, padding and overflow insets are re-sent when the pixel density changes (zero insets are skipped: they are zero pixels at any density), and paragraph state is rebuilt. Both text paths are covered: with enablePreparedTextLayout, the paragraph prepares a new layout at the new density (the prepared text cache is keyed by density) and its state now updates when only the density changed; without it, the state records the density too. The flag is off by default.

How to use: return true from your app's feature flag overrides, before React Native starts:

override fun fixTextLayoutOnPixelDensityChange(): Boolean = true

Changelog:
[Internal]

Reviewed By: javache

Differential Revision: D124076552

@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 13, 2026
@meta-codesync

meta-codesync Bot commented Oct 13, 2026

Copy link
Copy Markdown

@brunocramos has exported this pull request. If you are a Meta employee, you can view the originating Diff in D124076552.

Summary:

When an Android app handles density changes itself (`density` in `android:configChanges`), its surface is laid out again at the new density, but some mounted views keep sizes from the old one:

- Padding and overflow insets are sent to views in pixels, but only when their dp values change, which a density change usually doesn't do. A `TextInput` keeps its old pixel padding, so its text and placeholder can be clipped.
- Paragraph state only updates when the text, its attributes or its measured size change, so `Text` keeps the layout it built at the old density and draws at the old size.

With the new `fixTextLayoutOnPixelDensityChange` feature flag on, padding and overflow insets are re-sent when the pixel density changes (zero insets are skipped: they are zero pixels at any density), and paragraph state is rebuilt. Both text paths are covered: with `enablePreparedTextLayout`, the paragraph prepares a new layout at the new density (the prepared text cache is keyed by density) and its state now updates when only the density changed; without it, the state records the density too. The flag is off by default.

**How to use:** return `true` from your app's feature flag overrides, before React Native starts:

```
override fun fixTextLayoutOnPixelDensityChange(): Boolean = true
```

Changelog:
[Internal]

Reviewed By: javache

Differential Revision: D124076552
@meta-codesync meta-codesync Bot changed the title Refresh padding and text after a pixel density change Refresh padding and text after a pixel density change (#59049) Oct 13, 2026
@meta-codesync meta-codesync Bot closed this in d026c0f Oct 13, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Oct 13, 2026
@meta-codesync

meta-codesync Bot commented Oct 13, 2026

Copy link
Copy Markdown

This pull request has been merged in d026c0f.

meta-codesync Bot pushed a commit that referenced this pull request Oct 20, 2026
…59063)

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.

A kept layout is only reused at the pixel density it was prepared for. After a density change the paragraph prepares a new layout, so the state refresh from #59049 (`fixTextLayoutOnPixelDensityChange`) still applies with `enablePreparedTextLayout`.

Content with attachments (inline views inside the text) is not kept: the attachments point at the children the content was built from, and `layout()` replaces those children with laid-out clones, so in a later revision they would point at nodes only the previous revision owns.

**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

Pull Request resolved: #59063

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.
- The pixel-density check (last commit) was compiled against the Android build's flags with `-fsyntax-only` but not run; no density change was tested on a device.
- Galaxy A16 release build, cold start and 15 flings through two long feeds whose text contains inline views, 16 runs per variant: the first version of this PR crashed in 4 (SIGSEGV in `ParagraphShadowNode::layout`, reading an attachment the previous revision had released), the current version in none.

**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](https://claude.com/claude-code)

Reviewed By: cipolleschi

Differential Revision: D126131800

Pulled By: javache

fbshipit-source-id: 88d934a7ecb188430c19a6c62135f4602da7b99f
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. Merged This PR has been merged. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant