Skip the iOS ScrollView clip walk when no view clips its subviews #59284

Open
hryhoriiK97 opened 5:54am on October 21, 2026 wants to merge 1 commit into facebook/react-native main from
ios-skip-clip-walk-without-clipping

Pull Request Overview

  • Opened on October 21, 2026
  • Status Open
  • Commit count 1 with first commit October 21, 2026

Total Delta

0 Total Diff Delta

Open Days

Open 15 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Fraction of total time Business days Phase
 
0.0 days Authoring 1 commit before pull request opened for review
 
0.0 days Awaiting first review
 
16.0 days Revising work with 0 commits in response to 1 review that left 1 comment

Total time for pull request still awaiting merge (longer than repo's target): 16.0 business days

Author avatar

Skip the iOS ScrollView clip walk when no view clips its subviews

Summary:

RCTScrollViewComponentView updates clipped subviews in _remountChildren by calling updateClippedSubviewsWithClipRect:relativeToView: on its scroll view, after every mounting transaction and whenever the content offset has moved by more than kClippingLeeway while scrolling. The call walks every view in the scroll view's content. Only an RCTViewComponentView with removeClippedSubviews acts on it (it mounts and unmounts its children); every other view converts the rect and forwards it to its subviews.

Current behavior. removeClippedSubviews is off by default on iOS, so in most apps no view acts on the walk, but every ScrollView still runs it on every few points of scrolling. In RNTester, this walk is the largest main-thread cost while scrolling: from a sixth to almost half of the main thread's time on the screens below.

With this change. RCTViewComponentView keeps a count of live views with removeClippedSubviews on. It is updated when the prop changes, when a view is recycled and when it is deallocated, all on the main thread. _remountChildren returns early while the count is 0. When any view has removeClippedSubviews on, the walk runs as before. Turning the prop off doesn't depend on the walk either: the view remounts all its children as soon as the prop changes.

Changelog:

[IOS] [CHANGED] - A ScrollView no longer walks its content to update clipped subviews while no view has removeClippedSubviews on

Test Plan:



  • clang-format --dry-run --Werror on the changed files passes.


  • RCTViewComponentViewTests: all 24 tests pass on an iOS simulator. These tests are part of React-RCTFabric's test spec, which RNTester doesn't integrate, so I ran them by adding the file to the RNTesterUnitTests target locally (xcodebuild test -scheme RNTesterUnitTests -only-testing:RNTesterUnitTests/RCTViewComponentViewTests). The two new tests check that the count follows removeClippedSubviews across two views without counting a view twice, and that recycling and deallocating a clipping view take it off the count. With the decrement in dealloc removed, the second test fails.

  • 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 kept walking on every call and, whenever the count was 0, confirmed that no visited view had removeClippedSubviews on, there were 0 mismatches over 102,997 walks (ScrollViewExample 89,199, TextExample 11,799, FlatList basic 1,999). In a screen that turns removeClippedSubviews on and off twice, with views mounted and unmounted in between, the count returned to 0 each time with 0 mismatches.


  • Measurement. The two arms are the same binary with the change switched by a launch argument, in the order before, after, after, before. Each launch opens the screen and does 5 flings down and 5 up, driven by Argent on the physical iPhone, under Instruments' Time Profiler. Values are the main thread's sampled time per launch, averaged over the 2 launches of each arm. iPhone 15 Pro, Release:

RNTester screen, no removeClippedSubviews
before
this PR
change
of which the walk (before)

TextExample
2,954 ms
1,342 ms
βˆ’54.6%
1,326 ms

FlatList multicolumn
2,698 ms
1,498 ms
βˆ’44.5%
1,144 ms

ScrollViewExample
2,426 ms
1,679 ms
βˆ’30.8%
1,138 ms

FlatList basic
2,745 ms
1,963 ms
βˆ’28.5%
791 ms

ScrollViewSimpleExample
1,362 ms
1,094 ms
βˆ’19.7%
378 ms

SectionList scrollable
3,578 ms
3,356 ms
βˆ’6.2%
572 ms

With the change, the walk took 0 ms on every screen. SectionList is mostly bound by the JS thread, and its main-thread total varied by Β±10% between launches of the same arm.

RNTester Playground, 1000 cards inside a removeClippedSubviews view
before (5 launches)
this PR (6 launches)
change

main thread per launch, average (range)
2,345 ms (2,151–2,428)
2,411 ms (2,338–2,449)
within noise

With a clipping view present, both arms run the same walk and the only added work is one comparison per walk; the ranges overlap and the difference between launches comes from the flings.

πŸ€– Generated with Claude Code

Comments Threads Pending Resolution

javache reviewed on October 21, 2026
javache left a comment

I'm not a fan of using global state for this.

The subview clip pass should be shallow and only consider the children of the scrollview itself. Could we track this locally inside the scrollview's (content view?)?

Resolved Comment Threads

No resolved comments have been left on this PR.