Skip to content

Skip the unchanged scroll state update when a ScrollView mounts on Android - #59719

Open
hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-scrollview-skip-noop-state
Open

hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-scrollview-skip-noop-state

Conversation

@hryhoriiK97

Copy link
Copy Markdown
Contributor

Summary:

On Android, every ScrollView commits one state update on mount that changes nothing. onLayout calls scrollTo, which calls ReactScrollViewHelper.updateFabricScrollState. That method dedupes against lastStateUpdateScroll, but this starts at (-1, -1), so the first offset always gets through, even when it's the (0, 0) already in the state. The update then runs a full commit on the JS thread (clone, layout and differ) and sends an UpdateState item back to Java. On iOS, _updateStateWithContentOffset already skips an update when the offset hasn't changed.

Now, before a view's first update, updateFabricScrollState reads the offset from the state into lastStateUpdateScroll, and the existing dedupe does the rest. This costs one state read per view, not one per scroll frame.

I didn't start lastStateUpdateScroll at (0, 0), because a view created for a state that already holds another offset would then skip the update that corrects the state.

Changelog:

[ANDROID] [CHANGED] - ScrollView no longer commits an unchanged scroll state when it mounts

Test Plan:

Unit tests: the new ReactScrollViewHelperStateTest covers a first update that matches the state (not sent), one that differs from it (sent), one without state data (sent), and later updates (sent once per offset, with one state read). The two tests that expect a skipped update fail without this change.

./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests 'com.facebook.react.views.scroll.*'

25 tests, 0 failures. yarn format-kotlin makes no further changes.

Behavior: on a Galaxy A16, a temporary Playground screen read a marker's position with measureInWindow, and both builds reported the same positions at every step:

  • after mount with contentOffset, vertical and horizontal
  • after scrollTo, vertical and horizontal
  • after scrolling back to 0
  • after toggling display: none on the parent

Measurements on a Galaxy A16 (Android 16): two release RNTester APKs built from this commit's base, one with and one without this change, with default feature flags, compiled with cmd package compile -m speed. A temporary Playground screen mounts and unmounts the scenario 12 times. JS thread CPU comes from /proc/<pid>/task/<tid>/stat. The APKs were installed in alternating blocks, A/B/B/A twice, which gives 4 fresh processes per arm for each scenario. The table shows medians per mount and unmount:

Scenario Without this change With this change Change
100 ScrollViews, per ScrollView 1050 µs 554 µs −47%
Vertical ScrollView with 5 horizontal ones and 40 cards 86.6 ms 82.5 ms −4.8%
One ScrollView with 300 cards 307.0 ms 306.2 ms no change
FlatList with 40 cards 92.0 ms 92.9 ms no change

That's about 0.5–0.7 ms of JS time per ScrollView on this phone, so it only shows on screens with several of them. The 100-ScrollView case is artificial and shows the cost of each commit. In the second scenario the ranges overlap slightly (85.0–88.3 ms and 79.2–85.8 ms). UI thread time didn't change in any scenario.

Not tested on a device: enableViewRecyclingForScrollView and enableViewCulling. The display: none toggle may not recreate the view, so the case of a state that already holds another offset is covered only by the unit test. With enableViewCulling or useTraitHiddenOnAndroid on, loadFabricScrollState already sets lastStateUpdateScroll, so the new read doesn't run after that.

🤖 Generated with Claude Code

…droid

onLayout calls scrollTo, which calls updateFabricScrollState. Its dedupe
compares against lastStateUpdateScroll, which starts at (-1, -1), so the
first offset always gets through, even when it is already in the state.
That runs a full commit that changes nothing.

Before a view's first update, read the offset from the state into
lastStateUpdateScroll and let the existing dedupe decide.

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 31, 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 31, 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. 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