Skip to content

Trim NSAttributedString construction for text on iOS - #59154

Open
hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:ios-attributed-string-trims
Open

hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:ios-attributed-string-trims

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

RCTNSAttributedStringFromAttributedString (ReactCommon/react/renderer/textlayoutmanager/platform/ios/.../RCTAttributedTextUtils.mm) converts a paragraph's AttributedString into the NSAttributedString TextKit lays out and draws. It runs once per paragraph per measurement when the attributed-string cache misses, so for a screen of distinct labels it runs once per label.

Current behavior. Per fragment, the conversion:

  • iterated getFragments() with for (auto fragment : ...), copying each Fragment: its std::string, its TextAttributes, and a ShadowView with three shared_ptr members (props, event emitter, state), so three atomic retain/release pairs per fragment;
  • built the fragment's attributes, created the string, then added the event emitter in a second addAttributes:range: pass, which splits and re-merges the attribute runs TextKit just created;
  • allocated an NSMutableParagraphStyle before knowing whether any paragraph attribute was set, discarding it for the common unaligned, default-line-height case;
  • computed the effective font size multiplier twice when lineHeight is set, which with dynamicTypeRamp means two UIFontMetrics round trips;
  • always created an enclosing NSMutableAttributedString and appended each fragment to it, even for the single-fragment paragraph that most labels are;
  • built an NSString for the font family even when it is unset and resolves to the default family anyway.

With this change. Fragments are iterated by reference; the event emitter goes into the initial attribute dictionary; the paragraph style is created only when one of its fields is set; the multiplier is computed once and passed to the font resolution; a single-fragment paragraph returns the fragment's string directly; the family string is skipped when unset. The attributes attached to each run are the same as before, in the same ranges.

Changelog:

[IOS] [CHANGED] - Build a paragraph's NSAttributedString with fewer copies and allocations per text fragment

Test Plan:

  • node scripts/clang-format.js --check on the changed file passes.
  • RNTester built with xcodebuild for the iPhone 16 Pro simulator (Debug) and for a physical iPhone 15 Pro (Release); the Text examples render as before.
  • 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 mounting 1000 unique single-line and wrapping labels in one commit; a timer around the conversion call inside -[RCTTextLayoutManager measureAttributedString:...], summed per transaction. iPhone 16 Pro simulator, Debug, medians of 3 to 5 runs:
1000 labels before this PR change
conversion, system font 13.3 ms 10.7 ms −20%
conversion, fontFamily: 'Avenir' 12.2 ms 10.9 ms −11%

Conversion is about a quarter of the paragraph's measurement time, so this is roughly 5% of the shadow-thread layout cost of a text-heavy commit. TextKit layout itself (the remaining three quarters) is unchanged, as expected.

🤖 Generated with Claude Code

Building the NSAttributedString for a paragraph copied every fragment by value,
added the event emitter in a second pass over the fragment, wrapped a single
fragment in an enclosing mutable string, allocated a paragraph style whether or
not one was needed, and computed the font size multiplier twice. Do none of
that; the attributes produced per run are unchanged.

Co-Authored-By: Claude Fable 5.1 <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 16, 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 16, 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. 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