Repository navigation
Conversation
…nged _updateCellsToRender runs on every scroll event and enqueues a state updater that returns null when the window of rendered cells is the same, which is most of the time. React still re-renders the list to find that out. Run the same comparison before calling setState, while no other state update of the list is pending and once the list has been laid out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
javache
left a comment
There was a problem hiding this comment.
Duplicating React's own bookkeeping in _pendingStateUpdates does not seem like the right solution here.
It's more important that we ensure that state updater returns the same object identity when nothing meaningfully changed to avoid the re-render?
|
@javache Thanks for taking a look! I agree the extra bookkeeping doesn't sound good. However identity alone doesn't really get rid of the cost here:
I'm happy to iterate on the PR. We have two options:
Would either of those work better for you? |
|
A single flag would be a reasonable trade-off. Let's move the state-updater to a standalone function too, so we can prove it does not depend on anything in the update payload? Higher-complexity and riskier, but we should also evaluate moving this to a functional component so we can leverage the earlier bailout as follow-up. |
Track a queued state update with one boolean instead of a set of ids, and move the computation the cells updater does into a static _computeCellsUpdate(list, props, state), which the check before setState calls too, so both evaluate the same thing for the same state. Add a test that a scroll which keeps the render window doesn't call setState. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thanks for the suggestion! Done in 2af2fd0. I will re-run the benchmarks and update PR description. edit: Done
I'm happy to do that in a follow-up. |
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D125663171. |
Summary:
VirtualizedList._updateCellsToRenderruns for every scroll event, layout event and batch tick. It callssetStatewith an updater that recomputes the render window and returnsnullwhen nothing changed, which is most of the time while scrolling. The list itself doesn't re-render then, but for a class componentsetStatealways schedules a render pass, and React still has to run it to find that out. On a Galaxy A16 each of these passes takes about 4.5 ms. In a long feed with many mounted rows, those no-op updates were about 60% of all React updates during scrolling.This change moves the updater's computation into a static
_computeCellsUpdate(list, props, state), calls it beforesetStatetoo, and returns early when it returnsnull. The early return only applies when it gives the same answer as the updater would:this.stateis behind the state the updater will see._hasPendingStateUpdateis set before eachsetStateand cleared in the updater.onLayout, thenonContentSizeChange). The updater of the first call runs after both and sees the later metrics, so skipping that call would lose a batch._hiPriInProgressis cleared on an early return. It is normally cleared incomponentDidUpdate, which doesn't run without an update. Leaving it set would push the next high-priority update onto the batching timer.A first version without these conditions changed 8 snapshots in
VirtualizedList-test.js(render windows expanding one batch later). With them, all tests pass unchanged.Changelog:
[GENERAL] [CHANGED] - VirtualizedList: Skip the cells state update when the render window doesn't change
Test Plan:
yarn jest packages/virtualized-lists: 203 passed, 1 skipped, 69 snapshots unchanged. The new test, "does not update state on a scroll that keeps the render window", fails without the change.yarn flow check,eslintandprettierpass.Measured in a production app: React Native 0.86, release build, Galaxy A16, Perfetto. Each run traces 15 flings (about 30 s), interleaving runs with and without the change. Medians:
Profile feed (text, image and video rows), 5 runs each:
Home timeline, 6 runs each:
UI thread and RenderThread CPU are within 2% in both. Opening a screen with the list (3 runs each) creates the same 849 views with the same number of React updates, so the initial fill is not delayed.
🤖 Generated with Claude Code