Skip to content

perf: reduce SwiftUI update cascade during scroll#1

Draft
alex-duckmanton wants to merge 7 commits intomainfrom
fix/scroll-performance
Draft

perf: reduce SwiftUI update cascade during scroll#1
alex-duckmanton wants to merge 7 commits intomainfrom
fix/scroll-performance

Conversation

@alex-duckmanton
Copy link
Copy Markdown
Collaborator

Summary

  • Add equality guards to @State writes in BlockSpacing, TextBuilder, WithInlineStyle.output, and OrderedList.markerWidth — prevents redundant state mutations that trigger unnecessary view re-evaluations during scrolling
  • Consolidate two separate overlayPreferenceValue + GeometryReader pairs (from AttachmentOverlay and TextLinkInteraction) into a single TextFragmentOverlay modifier, halving the GeometryReader count per text fragment
  • Fix text selection not clearing on tap in iOS

Traced on iPhone 15 Pro in a long conversation (31 meeting summaries with nested markdown). Before these changes: 1.8M SwiftUI updates with a 543ms hang. The SecondaryChild<Text.LayoutKey, GeometryReader?> alone accounted for 72K updates.

Test plan

  • Verify markdown rendering in chat messages (paragraphs, lists, headings, bold, links, thematic breaks)
  • Verify link tapping still works in rendered text
  • Verify attachment overlays render correctly
  • Scroll through a long conversation and confirm no visual regressions
  • Profile with Instruments SwiftUI template to confirm reduced update counts

🤖 Generated with Claude Code

saad-relevanceai and others added 6 commits March 12, 2026 15:32
On iOS, tapping a non-URL area while text is selected does nothing —
the selection persists until the user double-taps another word. The
macOS equivalent (mouseDown) correctly calls resetSelection().

Clear model.selectedRange on non-URL taps to match macOS behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip redundant @State writes in BlockLayoutView.onPreferenceChange
when the resolved block spacing value hasn't changed. Eliminates ~295K
unnecessary SwiftUI state invalidations during scroll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Track currentAttachmentSizes with @ObservationIgnored and skip
sizeChanged when the computed sizes match. Eliminates ~204K redundant
@observable mutations during scroll that cascaded into TextFragment
body re-evaluations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip redundant @State writes when the resolved AttributedString output
hasn't changed. Eliminates ~162K unnecessary state invalidations that
cascaded into TextFragment rebuilds during scroll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip redundant @State writes when the marker width preference value
hasn't changed. Prevents cascading preference updates in ordered lists
during scroll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace separate AttachmentOverlay + TextLinkInteraction modifiers with a
single TextFragmentOverlay that uses one overlayPreferenceValue subscription
and one GeometryReader per text fragment instead of two of each. This halves
the GeometryReader count and should significantly reduce the update cascade
during scrolling (72K SecondaryChild updates in trace).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alex-duckmanton alex-duckmanton marked this pull request as draft March 30, 2026 06:46
These modifiers were superseded by the consolidated TextFragmentOverlay.
Updates stale comments in TextFragment, TextBuilder, and System Overview
to reference the new modifier.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants