Repository navigation
Skip the iOS ScrollView clip walk outside a window - #59924
Open
JacquesLeupin wants to merge 1 commit into
Open
JacquesLeupin wants to merge 1 commit into
JacquesLeupin wants to merge 1 commit into
Conversation
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.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
After every mounting transaction, each mounted
RCTScrollViewComponentViewruns_remountChildren, which walks every descendant view to update clipped subviews. That includes ScrollViews that aren't in a window: everything under a full-screenModal, screens covered by a pushed screen in a native stack, and lists inside rows that an outerremoveClippedSubviewslist 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,
didMoveToWindowasks for a layout pass andlayoutSubviewsruns the walk once, before the view is displayed (the same patternRCTViewComponentViewuses for safe area insets). Inside a window nothing changes. The only difference I know of is for code that reads the subviews of aremoveClippedSubviewsview 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
RCTScrollViewComponentViewTestscases: 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/Testsisn't built by RNTester, so I ran them with XCTest on an iOS 27 simulator by compiling the test file, with and without the patchedRCTScrollViewComponentView.mm, against the 0.88.1 ReleaseReact.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
removeClippedSubviewslist whose screen gets covered by aUINavigationControllerpush 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 isdidMoveToWindow,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
removeClippedSubviewsanywhere: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-formatis clean.