Skip to content

Commit 7fc58f2

Browse files
hryhoriiK97meta-codesync[bot]
authored andcommitted
Avoid copying text fragments and re-attributing runs when building NSAttributedString (#59154)
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 Pull Request resolved: #59154 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](https://github.com/software-mansion/argent) on the physical iPhone. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: cipolleschi Differential Revision: D126056330 Pulled By: javache fbshipit-source-id: 620dfa63b57e905f98ed57a1e304d14db9915e72
1 parent 6e2edef commit 7fc58f2

1 file changed

Lines changed: 12 additions & 22 deletions

File tree

  • packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager

‎packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm‎

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,9 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
405405
const AttributedString::Fragment &fragment,
406406
UIImage *placeholderImage)
407407
{
408+
NSString *string;
409+
NSMutableDictionary<NSAttributedStringKey, id> *attributes;
410+
408411
if (fragment.isAttachment()) {
409412
auto layoutMetrics = fragment.parentShadowView.layoutMetrics;
410413
CGRect bounds = {
@@ -415,41 +418,28 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
415418
attachment.image = placeholderImage;
416419
attachment.bounds = bounds;
417420

418-
return [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy];
421+
string = [NSString stringWithFormat:@"%C", (unichar)NSAttachmentCharacter];
422+
attributes = [NSMutableDictionary dictionaryWithObject:attachment forKey:NSAttachmentAttributeName];
419423
} else {
420424
NSString *decoded = [[NSString alloc] initWithBytes:fragment.string.data()
421425
length:fragment.string.size()
422426
encoding:NSUTF8StringEncoding];
423-
NSString *string = decoded != nil ? decoded : @"";
427+
string = decoded != nil ? decoded : @"";
424428

425429
if (fragment.textAttributes.textTransform.has_value()) {
426430
auto textTransform = fragment.textAttributes.textTransform.value();
427431
string = RCTNSStringFromStringApplyingTextTransform(string, textTransform);
428432
}
429433

430-
return [[NSMutableAttributedString alloc]
431-
initWithString:string
432-
attributes:RCTNSTextAttributesFromTextAttributes(fragment.textAttributes)];
434+
attributes = RCTNSTextAttributesFromTextAttributes(fragment.textAttributes);
433435
}
434-
}
435-
436-
static NSMutableAttributedString *RCTNSAttributedStringFragmentWithAttributesFromFragment(
437-
const AttributedString::Fragment &fragment,
438-
UIImage *placeholderImage)
439-
{
440-
auto nsAttributedStringFragment = RCTNSAttributedStringFragmentFromFragment(fragment, placeholderImage);
441436

437+
// Set with the initial attributes; adding it afterwards would re-split the runs.
442438
if (fragment.parentShadowView.componentHandle) {
443-
auto eventEmitterWrapper = RCTWrapEventEmitter(fragment.parentShadowView.eventEmitter);
444-
445-
NSDictionary<NSAttributedStringKey, id> *additionalTextAttributes =
446-
@{RCTAttributedStringEventEmitterKey : eventEmitterWrapper};
447-
448-
[nsAttributedStringFragment addAttributes:additionalTextAttributes
449-
range:NSMakeRange(0, nsAttributedStringFragment.length)];
439+
attributes[RCTAttributedStringEventEmitterKey] = RCTWrapEventEmitter(fragment.parentShadowView.eventEmitter);
450440
}
451441

452-
return nsAttributedStringFragment;
442+
return [[NSMutableAttributedString alloc] initWithString:string attributes:attributes];
453443
}
454444

455445
NSAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString)
@@ -464,9 +454,9 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
464454

465455
[nsAttributedString beginEditing];
466456

467-
for (auto fragment : attributedString.getFragments()) {
457+
for (const auto &fragment : attributedString.getFragments()) {
468458
NSMutableAttributedString *nsAttributedStringFragment =
469-
RCTNSAttributedStringFragmentWithAttributesFromFragment(fragment, placeholderImage);
459+
RCTNSAttributedStringFragmentFromFragment(fragment, placeholderImage);
470460

471461
[nsAttributedString appendAttributedString:nsAttributedStringFragment];
472462
}

0 commit comments

Comments
 (0)