Skip to content

Skip the iOS ScrollView clip walk outside a window - #59924

Open
JacquesLeupin wants to merge 1 commit into
react:mainfrom
JacquesLeupin:fix/ios-scrollview-skip-clip-walk-outside-window
Open

JacquesLeupin wants to merge 1 commit into
react:mainfrom
JacquesLeupin:fix/ios-scrollview-skip-clip-walk-outside-window

Conversation

@JacquesLeupin

Copy link
Copy Markdown

Summary:

After every mounting transaction, each mounted RCTScrollViewComponentView runs _remountChildren, which walks every descendant view to update clipped subviews. That includes ScrollViews that aren't in a window: everything under a full-screen Modal, screens covered by a pushed screen in a native stack, and lists inside rows that an outer removeClippedSubviews list has detached. So the cost of a transaction grows with every screen that is still mounted, not with what is on screen.

We see this in production: 57 fatal app hangs in the last 30 days with the main thread in this walk under mountingTransactionDidMount:, nearly all of them on a form or picker pushed over other screens, where each commit while typing walks the lists of the screens underneath.

With this change a ScrollView outside a window skips the walk and remembers that it did. When it moves to a window, didMoveToWindow asks for a layout pass and layoutSubviews runs the walk once, before the view is displayed (the same pattern RCTViewComponentView uses for safe area insets). Inside a window nothing changes. The only difference I know of is for code that reads the subviews of a removeClippedSubviews view while its ScrollView is off screen.

Related: #59284 skips the walk for ScrollViews in a window that have no clipping views and keeps walking outside a window, so the two are complementary. #57414 gates on the transaction's mutations instead.

Changelog:

[IOS] [CHANGED] - A ScrollView that is not in a window no longer walks its content to update clipped subviews after every mounting transaction

Test Plan:

Two new RCTScrollViewComponentViewTests cases: in a window, clipped subviews are updated immediately; outside a window the update waits and is done when the ScrollView is laid out in one.

React/Tests isn't built by RNTester, so I ran them with XCTest on an iOS 27 simulator by compiling the test file, with and without the patched RCTScrollViewComponentView.mm, against the 0.88.1 Release React.framework (the code touched here is the same on 0.88.1 and main). Without the change the new off-window case fails; with it everything passes.

I also put the same framework in a small UIKit app, with a removeClippedSubviews list whose screen gets covered by a UINavigationController push and by a full-screen modal. In both cases the covered ScrollView is outside the window. Before, 100 transactions while covered cost 100 walks; with the change they cost none, and there is one walk when the screen comes back. On the way back (pop with and without animation, modal dismiss) the order is didMoveToWindow, layoutSubviews, then the frame is committed, and rows mounted or moved while covered are already right in that first frame.

Time for one walk (one ScrollView, one transaction) with no removeClippedSubviews anywhere:

views under the ScrollView before, in or out of a window this PR, outside a window
1,001 0.10 ms 0
5,001 0.54 ms 0
20,001 2.5 ms 0

That is a simulator on an M-series Mac; for scale, #59246 measured 135 µs for 123 views on an iPhone 15 Pro. Inside a window the time is unchanged.

The patched file and the tests compile against main, and clang-format is clean.

After every mounting transaction, each mounted RCTScrollViewComponentView
walks all of its descendant views to update clipped subviews, including
ScrollViews that are not in a window: screens covered by a full-screen
Modal or a pushed navigation screen, and lists inside rows that an outer
list has clipped out.

Skip the walk while the ScrollView is outside a window and run it once,
from layoutSubviews, when it moves to one.
@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 Nov 10, 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 Nov 10, 2026

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.

1 participant