Skip to content

fix(scroll): render the first items where initialScrollIndex lands - #2516

Open
anubhavvI2B wants to merge 1 commit into
Shopify:mainfrom
anubhavvI2B:fix/initial-scroll-first-render-offset
Open

anubhavvI2B wants to merge 1 commit into
Shopify:mainfrom
anubhavvI2B:fix/initial-scroll-first-render-offset

Conversation

@anubhavvI2B

@anubhavvI2B anubhavvI2B commented Sep 28, 2026 •

Copy link
Copy Markdown

Description

Follow-up to #1870.

With initialScrollIndex, the first items are rendered progressively before the list scrolls. applyInitialScrollAdjustment plans that first render from the initial item's own offset, but applyInitialScrollIndex then scrolls to that offset plus initialScrollIndexParams.viewOffset, and the ScrollView clamps the result to its scrollable range. When the two differ, the items between the viewport edge and the initial item are not part of the first render and stay blank until the next pass renders the draw-distance buffer:

  • A negative viewOffset (initial item shown lower in the viewport): the rows before the initial item are blank.
  • An initial item inside the last screen, with or without viewOffset (for example startRenderingFromBottom): the list cannot scroll to the item's own offset, so the rows before it are blank.

This change plans the first render window at the offset the list actually lands on: the item offset plus viewOffset, clamped to [-firstItemOffset, getMaxScrollOffset() - firstItemOffset]. Without a viewOffset, and away from the end of the list, the result is unchanged.

We hit this in an inverted chat list that opens at a search result with initialScrollIndex and a negative viewOffset: the target row painted first and the rows below it stayed white until the buffer pass (about 1 s in a dev build, about 3 s on a cold start). With an equivalent local patch on 2.2.2, the first frame shows the full screen around the target.

Reviewers' hat-rack 🎩

  • applyInitialScrollAdjustment in RecyclerViewManager.ts. The upper clamp uses getMaxScrollOffset(), which relies on estimated sizes until items are measured; each progressive pass recomputes it, the same way it recomputes the item offset.
  • Behaviour change without viewOffset: only when the initial item is inside the last screen, where the first render now covers that screen instead of starting at the item.

Screenshots or videos

None. The three new unit tests cover the cases above.

Test plan

  • Unit tests pass (yarn test). The 3 new tests in RecyclerView.test.tsx fail on main and pass with this change.
  • Type check passes (yarn type-check)
  • Lint passes on the changed files
  • Verified the viewOffset case on an Android emulator in our app, with an equivalent patch on 2.2.2 (applied only when viewOffset is set)
  • Not run in the fixture app

The progressive first render planned its window from the initial item's
own offset, while applyInitialScrollIndex scrolls to that offset plus
initialScrollIndexParams.viewOffset, clamped by the ScrollView to its
scrollable range. With a negative viewOffset, or an initial item inside
the last screen, the items between the viewport edge and the initial
item stayed blank until the next render pass.

Plan the first render window at the same clamped offset.
@anubhavvI2B
anubhavvI2B force-pushed the fix/initial-scroll-first-render-offset branch from 9b6826a to 1aa2f32 Compare September 28, 2026 21:39
@anubhavvI2B

Copy link
Copy Markdown
Author

I have signed the CLA!

@anubhavvI2B

Copy link
Copy Markdown
Author

I have signed the CLA!

Hey @tobi can you review this once

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant