Skip to content

Avoid copying text fragments and re-attributing runs when building NSAttributedString - #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. Two pieces of redundant work per fragment:

  • the loop iterates getFragments() by value (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;
  • the event emitter is attached in a second addAttributes:range: pass over the fragment's string right after it was created with its attributes, which splits and re-merges the attribute runs TextKit just built. For an attachment, the string from attributedStringWithAttachment: is also copied with mutableCopy before that pass.

With this change. Fragments are iterated by reference, and RCTNSAttributedStringFragmentFromFragment creates each fragment's string with all of its attributes at once, including the event emitter, for text and attachment fragments alike. The second addAttributes:range: pass and the wrapper function that ran it are gone. The attachment string is created with initWithString:attributes: from NSAttachmentCharacter and NSAttachmentAttributeName, which is what attributedStringWithAttachment: produces, so it needs no copy. Every run ends up with the same attributes as before.

Changelog:

[IOS] [CHANGED] - Build a paragraph's NSAttributedString without copying each text fragment or re-attributing its runs

Test Plan:

  • node scripts/clang-format.js --check on the changed file passes.
  • RNTester built with xcodebuild in Release and run on a physical iPhone 15 Pro (iOS 26.6.1).
  • Same output. A local build ran main's conversion next to this one for every paragraph and compared the two results run by run: string, run ranges and every attribute, with attachments compared by bounds and image and event emitters by the emitter they wrap. Scrolling through the RNTester Text examples, which include inline views and pressable nested text, and mounting 1000 labels five times in the Playground compared over 31,000 conversions, with 217 attachment fragments and over 32,000 fragments carrying an event emitter. There were no differences, and the same check reports an emitter removed on purpose.
  • Measurement. In the same build, both conversions were timed on the same input, alternating which ran first. Mounting 1000 system-font labels five times in the RNTester Playground (27,750 conversions): 135.3 ms with main's conversion and 99.4 ms with this change, −26% of the conversion step. The presses were driven by Argent on the physical iPhone.

🤖 Generated with Claude Code

@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

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is doing a lot of changes that are unrelated from the PR title/purpose.
Also, most of them are changes that are not really needed: they are small refactoring that only adds complexity and take time to review.

Please do small focused PRs that do a single thing and do not add not-relevant changes to the PR

Comment on lines +423 to +427
[[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy];
if (eventEmitterWrapper != nil) {
[nsAttributedStringFragment addAttribute:RCTAttributedStringEventEmitterKey
value:eventEmitterWrapper
range:NSMakeRange(0, nsAttributedStringFragment.length)];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changes the behavior of React Native and has nothing to do with the PR title.

Before we were not attaching the event emitter if the fragment was an attachment, now we are.

If you want this change, please create a separate PR with the proper title and summary. Let's have small and focused PRs, please.

@hryhoriiK97 hryhoriiK97 Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

I believe this one is not a behavior change: on main, the same addAttributes call already set the emitter on attachment fragments too (RCTNSAttributedStringFragmentWithAttributesFromFragment applied it to both kinds). To make that clearer, I've reworked the diff so the attachment code stays untouched - only text fragments now receive the emitter in their initial attributes. Happy to adjust if you'd prefer a different shape 🙂

@hryhoriiK97
hryhoriiK97 force-pushed the ios-attributed-string-trims branch from 1343122 to 3fee4c7 Compare October 17, 2026 11:34
@hryhoriiK97 hryhoriiK97 changed the title Trim NSAttributedString construction for text on iOS Avoid copying text fragments and re-attributing runs when building NSAttributedString Oct 17, 2026
@hryhoriiK97
hryhoriiK97 force-pushed the ios-attributed-string-trims branch 2 times, most recently from dc8e2cc to 37b84be Compare October 17, 2026 11:47
@hryhoriiK97

Copy link
Copy Markdown
Contributor Author

This PR is doing a lot of changes that are unrelated from the PR title/purpose. Also, most of them are changes that are not really needed: they are small refactoring that only adds complexity and take time to review.

Please do small focused PRs that do a single thing and do not add not-relevant changes to the PR

@cipolleschi sorry about this! I've updated the PR so it does a single thing: it only avoids copying each text fragment and re-attributing the runs when building the NSAttributedString. The unrelated refactors are removed, and the title, description and measurements are updated to match the smaller diff. Thanks for the review!

Comment on lines 453 to 454
NSDictionary<NSAttributedStringKey, id> *additionalTextAttributes =
@{RCTAttributedStringEventEmitterKey : eventEmitterWrapper};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not handle this inside RCTNSAttributedStringFragmentFromFragment too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, done. The event emitter is now set inside RCTNSAttributedStringFragmentFromFragment for both text and attachments, so the extra addAttributes pass and the wrapper function are gone.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to return a mutableCopy? It's already mutable and there's no other references

@hryhoriiK97 hryhoriiK97 Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! I now build the attachment string directly with initWithString:attributes:, so there's no copy. The output is identical.

[nsAttributedString beginEditing];

for (auto fragment : attributedString.getFragments()) {
for (const auto &fragment : attributedString.getFragments()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

…AttributedString

Iterate AttributedString fragments by reference instead of copying each
Fragment, and build each fragment's NSAttributedString with all of its
attributes at once, including the event emitter, for text and attachment
fragments alike. This removes the second addAttributes:range: pass, the
wrapper function that ran it, and the mutableCopy of the attachment string,
which is now created directly with initWithString:attributes:.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hryhoriiK97
hryhoriiK97 force-pushed the ios-attributed-string-trims branch from 37b84be to b2188ae Compare October 19, 2026 11:19
@hryhoriiK97
hryhoriiK97 requested a review from javache October 19, 2026 11:21
@meta-codesync

meta-codesync Bot commented Oct 19, 2026

Copy link
Copy Markdown

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

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.

3 participants