Repository navigation
Commit 631fd89
Skip redrawing iOS Text when only its position changes (#59155)
Summary:
`RCTParagraphComponentView` draws its text in a child `RCTParagraphTextView`, positioned at the paragraph's content frame within the view's own bounds.
**Current behavior.** The view requests a redraw of its text on every layout metrics update and on every state update. Both fire for a paragraph that has merely moved:
- the mounting layer sends layout metrics whenever any metric differs, including the frame's origin alone;
- a relayout gives the paragraph a new state even when its text is unchanged, because `AttributedString` equality includes each fragment's `parentShadowView`, whose layout metrics changed, so `ParagraphShadowNode::updateStateIfNeeded` sees a different string.
So when the content above a paragraph changes height, every paragraph below it re-runs TextKit layout and glyph rasterization, although its bounds, padding and text are unchanged. A row inserted at the top of a non-virtualized list redraws every label under it.
**With this change.** A redraw is requested only when something the drawing depends on changed:
- in `updateLayoutMetrics:`, the view's frame size, its content frame (which carries the content insets the drawing origin is derived from), or the pixel scale factor;
- in `updateState:`, the text content (`AttributedString::isContentEqual`, which compares strings and text attributes but not layout metrics) or the paragraph attributes. A string containing inline views keeps redrawing on every state, since attachment sizes come from the layout metrics the content comparison ignores.
A recycled view always receives a new state before it is displayed, and the first state of a view always redraws, so neither comparison can skip a needed redraw.
**Why.** Text drawing is the most expensive per-view work on the main thread during a reflow, and none of it is needed when the paragraph only moved.
## Changelog:
[IOS] [FIXED] - Text no longer redraws when only its position changes, such as when content above it changes height
Pull Request resolved: #59155
Test Plan:
- `node scripts/clang-format.js --check` on the changed file passes.
- RNTester built with `xcodebuild` in Release for a physical iPhone 15 Pro; the Text examples render as before, including after content above a paragraph changes height, after a paragraph's own size changes, and after a text change with unchanged metrics.
- Screenshots of a 1000-label list and of a set of edge cases (right-aligned text with an inline view, centered multi-line text, empty text with an explicit size, truncation across nested colours, RTL with an inline view, clipped fixed height, two-line tail ellipsis) are byte-identical before and after.
- **Measurement.** RNTester Playground with 1000 unique single-line and wrapping labels mounted in a `ScrollView`; a "reflow" toggles the height of a spacer above the labels from 0 to 40 pt, so every label moves and none changes size. Timed from Fabric's transaction telemetry, a run-loop observer after the CoreAnimation commit (mount end to display), a timer around `-[RCTParagraphTextView drawRect:]`, and the `touchend` Event Timing entry for the press (press to mount end). The two arms are the same binary with the new checks switched by an environment variable. iPhone 15 Pro, Release, medians of 5 presses:
| reflow with 1000 labels mounted | before | this PR | change |
|---|--:|--:|--:|
| text redraws | 1001 | 1 | |
| `drawRect` total, system font | 63.0 ms | 0.7 ms | −99% |
| `drawRect` total, `fontFamily: 'Avenir'` | 51.1 ms | 0.1 ms | −100% |
| mount end to display, system font | 115.6 ms | 1.7 ms | −99% |
| mount end to display, Avenir | 106.7 ms | 1.0 ms | −99% |
| press to mount end, system font | 180.1 ms | 67.4 ms | −63% |
| press to mount end, Avenir | 176.4 ms | 68.0 ms | −61% |
The one remaining redraw is the harness's own status label, whose text changes. The table above is from manual presses. The same sequence was then driven by [Argent](https://github.com/software-mansion/argent) on the device: with this change it again redrew 0 to 4 labels per reflow and measured 33 to 74 ms press-to-mount across ten reflows; without it, the reflows that redrew measured 150 to 200 ms press-to-mount, matching the manual figures. Mount and unmount figures agreed between the two methods within noise. Press-to-mount also drops because the main thread no longer spends the previous frame drawing before it can run the next mount. Mounting, unmounting and text changes are unchanged within noise: mounting the same 1000 labels measures 113 ms to display in both arms.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed By: cipolleschi
Differential Revision: D126286390
Pulled By: javache
fbshipit-source-id: 33d6ee192672321f5f9990ec23b435e068ee97fc1 parent ac766e6 commit 631fd89
1 file changed
Lines changed: 35 additions & 3 deletions
File tree
- packages/react-native/React/Fabric/Mounting/ComponentViews/Text
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
37 | 47 | | |
38 | 48 | | |
39 | 49 | | |
| |||
135 | 145 | | |
136 | 146 | | |
137 | 147 | | |
138 | | - | |
139 | | - | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
140 | 165 | | |
141 | 166 | | |
142 | 167 | | |
| |||
161 | 186 | | |
162 | 187 | | |
163 | 188 | | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
164 | 194 | | |
165 | 195 | | |
166 | 196 | | |
167 | 197 | | |
168 | | - | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
169 | 201 | | |
170 | 202 | | |
171 | 203 | | |
| |||
0 commit comments