From 6d5de4928c5ef09f5d4eac2102a622078d010940 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Sat, 17 Oct 2026 01:10:24 +0200 Subject: [PATCH] Skip redrawing iOS Text when only its position changes `RCTParagraphComponentView` requested a redraw on every layout metrics update and on every state update. Both fire for a paragraph that merely moved: the mounting layer sends layout metrics whenever the origin changes, and a relayout gives the paragraph a new state even with unchanged text, because the attributed string records each fragment's layout metrics. A reflow above a list therefore redrew every paragraph below it. Text is drawn relative to the view's own bounds and content frame, from the content and paragraph attributes in its state. Redraw only when one of those, or the pixel density, changed. Strings with inline views keep redrawing on every state, since attachment sizes come from the layout metrics the content comparison ignores. Co-Authored-By: Claude Fable 5.1 --- .../Text/RCTParagraphComponentView.mm | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index da6658cc1319..ddb7679f77d4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -34,6 +34,16 @@ - (CGRect)drawingFrameForAttributedString:(const facebook::react::AttributedStri @end +static BOOL RCTAttributedStringHasAttachments(const AttributedString &attributedString) +{ + for (const auto &fragment : attributedString.getFragments()) { + if (fragment.isAttachment()) { + return YES; + } + } + return NO; +} + // ParagraphTextView is an auxiliary view we set as contentView so the drawing // can happen on top of the layers manipulated by RCTViewComponentView (the parent view) @interface RCTParagraphTextView : UIView @@ -135,8 +145,22 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState { - _textView.state = std::static_pointer_cast(state); - [_textView setNeedsDisplay]; + auto newState = std::static_pointer_cast(state); + // A relayout yields a new state even for unchanged text (the attributed + // string records fragment layout metrics); redraw only when content or + // paragraph attributes changed. Attachments are sized from those metrics. + BOOL needsDisplay = YES; + if (newState && oldState && _textView.state) { + const auto &newData = newState->getData(); + const auto &oldData = std::static_pointer_cast(oldState)->getData(); + needsDisplay = !newData.attributedString.isContentEqual(oldData.attributedString) || + newData.paragraphAttributes != oldData.paragraphAttributes || + RCTAttributedStringHasAttachments(newData.attributedString); + } + _textView.state = newState; + if (needsDisplay) { + [_textView setNeedsDisplay]; + } // If the attributed string has changed, we need to notify the accessibility system that something changed, // otherwise it may hold on to stale values (this happens most often when an element is updated async) @@ -161,11 +185,18 @@ - (void)updateState:(const State::Shared &)state oldState:(const State::Shared & - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics { + // Drawing depends on size, content frame and scale, not on position. + BOOL needsDisplay = _layoutMetrics.frame.size != layoutMetrics.frame.size || + _layoutMetrics.getContentFrame() != layoutMetrics.getContentFrame() || + _layoutMetrics.pointScaleFactor != layoutMetrics.pointScaleFactor; + // Using stored `_layoutMetrics` as `oldLayoutMetrics` here to avoid // re-applying individual sub-values which weren't changed. [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics]; _textView.layoutMetrics = _layoutMetrics; - [_textView setNeedsDisplay]; + if (needsDisplay) { + [_textView setNeedsDisplay]; + } } - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask