Skip to content

Pass the clip rect through iOS views that don't clip subviews - #59246

Closed
hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:ios-clip-forward-rect
Closed

hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:ios-clip-forward-rect

Conversation

@hryhoriiK97

Copy link
Copy Markdown
Contributor

Summary:

RCTScrollViewComponentView updates clipped subviews by calling updateClippedSubviewsWithClipRect:relativeToView: on its scroll view, after every mounting transaction and while scrolling. The call walks every view in the scroll view's content: views with removeClippedSubviews (RCTViewComponentView) unmount the children outside the rect, and every other view forwards the call to its subviews through the default implementation in UIView+ComponentViewProtocol.mm.

Current behavior. The default implementation converts the rect into its own coordinates (convertRect:toView:) and passes it on relative to itself, so every view on the way does one conversion, although none of them uses the rect. The only views that use it, those with removeClippedSubviews, convert it from the view it is relative to themselves. In a FlatList without removeClippedSubviews (the default on iOS), every walk converts the rect once per view in the visible content and uses none of the results.

With this change. A view that doesn't clip passes the rect on unchanged, still relative to the same view. A view with removeClippedSubviews converts it from that view as before, in one conversion instead of one per level. For translations and scales the result is the same; with two or more rotated or skewed views between the scroll view and a clipping view, one conversion gives the exact bounding box where converting level by level grows it at each level, so such a view clips slightly more precisely.

Changelog:

[IOS] [CHANGED] - Updating clipped subviews of a ScrollView no longer converts the clip rect in every view it passes through

Test Plan:

  • clang-format --dry-run --Werror on the changed file passes.
  • RNTester built with xcodebuild in Release and run on a physical iPhone 15 Pro (iOS 26.6.1).
  • Same clipping. With a local check that computed, on every walk, the rect each view with removeClippedSubviews receives both ways (converted at every level, and passed through unchanged and converted once), 6,394 rects over 3,205 walks while flinging 1000 cards inside a removeClippedSubviews view, behind an offset wrapper, were equal within 0.01 pt. With the wrapper rotated by 2°, 6,358 rects over 3,187 walks were equal as well.
  • Overrides. On GitHub, the other overrides of this method I found (expo-modules-core's SwiftUI virtual views, react-native-nano-icons) ignore the rect. An override that converts the rect from clipView, as RCTViewComponentView does, is unaffected; one that assumed clipView is its direct superview would now get an ancestor.
  • Measurement. The two arms are the same binary with the change switched by a launch argument, in the order before, after, after, before. Flings were driven by Argent on the physical iPhone. Timed: the whole walk in _remountChildren. iPhone 15 Pro, Release:
RNTester Playground, a FlatList of 500 multi-line messages without removeClippedSubviews, 20 flings down and 20 up per launch (18,000 walks, 123 views each) before this PR change
walk time per launch 2,438 ms 1,163 ms −52.3%
walk time per walk 135.4 µs 64.6 µs −52.3%
RNTester Playground, 1000 cards inside a removeClippedSubviews view, 16 flings down and 16 up per launch (4,000 walks, 30 views each) before this PR change
walk time per launch 2,372 ms 2,349 ms within noise

When the content clips, the walk stops at the clipping view and its time goes into comparing and remounting that view's children, so there is little to save.

🤖 Generated with Claude Code

When a ScrollView updates clipped subviews, every view on the way down converts
the clip rect into its own coordinates and passes it on, although only views
with removeClippedSubviews use it, and those convert it from the view it is
relative to themselves. Views that don't clip now pass the rect on unchanged,
still relative to the same view, so a clipping view converts it once instead
of once per level.

Co-Authored-By: Claude Opus 5.5 <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 20, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 20, 2026
@hryhoriiK97

Copy link
Copy Markdown
Contributor Author

Closing in favor of #59284, which skips this walk entirely when no view has removeClippedSubviews on. When a view does, this change measured within noise.

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. p: Software Mansion Partner: Software Mansion Partner 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