Skip to content

perf(timeline): restore VList cache + skip 80 ms timer on room revisit#659

Open
Just-Insane wants to merge 6 commits intoSableClient:devfrom
Just-Insane:perf/timeline-scroll-cache
Open

perf(timeline): restore VList cache + skip 80 ms timer on room revisit#659
Just-Insane wants to merge 6 commits intoSableClient:devfrom
Just-Insane:perf/timeline-scroll-cache

Conversation

@Just-Insane
Copy link
Copy Markdown
Contributor

Description

Caches VList item heights (via roomScrollCache) across visits to the same room within a session. On revisit, the stored cache is provided to VList upfront and the scroll position is restored immediately — skipping the 80 ms opacity-fade stabilisation timer entirely.

Also fixes a bug where the pendingReadyRef recovery path (triggered when the 80 ms timer fires while processedEvents is empty due to a TimelineReset race) never saved the scroll cache, causing rooms that hit this path to show the fade-in on subsequent visits.

Fixes #

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

AI disclosure:

  • Fully AI generated (explain what all the generated code does in moderate detail).

roomScrollCache.ts — a simple session-scoped Map<roomId, { cache, scrollOffset, atBottom }>. cache is VList's CacheSnapshot (measured item heights), scrollOffset is the pixel offset at save time, and atBottom is a flag used to decide whether to call scrollToIndex (bottom) or scrollTo (exact offset) on restore.

In RoomTimeline.tsx, the initial-scroll useLayoutEffect was extended to check for a saved cache before starting the 80 ms timer: if a cache exists, it restores position immediately and calls setIsReady(true) without the delay. The 80 ms timer path and the pendingReadyRef recovery path both now call roomScrollCache.save(...) after scrolling so subsequent visits have a cache to load.

When navigating back to a previously-visited room, save the VList
CacheSnapshot (item heights) and scroll offset on the way out, then
on the way back:
  • pass the snapshot as cache= to VList so item heights do not need
    to be remeasured (VList is keyed by room.roomId so it gets a fresh
    instance with the saved measurements)
  • skip the 80 ms stabilisation timer — the measurements are already
    known, so the scroll lands immediately and setIsReady(true) is
    called without the artificial delay

First-visit rooms retain the existing 80 ms behaviour unchanged.
RoomTimeline mounts fresh per room (key={roomId} in RoomView), so the
render-phase room-change block used for save/load never fires.

- Init scrollCacheForRoomRef from roomScrollCache.load() on mount so the
  CacheSnapshot is actually provided to VList on first render.
- Save the cache in handleVListScroll (and after the first-visit 80 ms
  timer) rather than in the unreachable room-change block.
- Trim the room-change block to just the load + state-reset path (kept as
  a defensive fallback for any future scenario where room prop changes
  without remount).
@Just-Insane Just-Insane requested review from 7w1 and hazre as code owners April 10, 2026 02:52
@Just-Insane
Copy link
Copy Markdown
Contributor Author

Added a follow-up fix in the latest commit: setAtBottom(true) is now called synchronously when scrolling to the bottom in the cache-restore and pendingReadyRef recovery paths, immediately before setIsReady(true). This prevents the "Jump to Latest" button from flashing for one render cycle between isReady becoming true and VList's async onScroll confirming the bottom position.

…mmatic scroll-to-bottom

After setIsReady(true) commits, virtua can fire onScroll events with
isNowAtBottom=false during its height-correction pass (particularly on
first visit when item heights above the viewport haven't been rendered
yet). These intermediate events were driving atBottomState to false
while isReady=true, flashing the 'Jump to Latest' button.

Add programmaticScrollToBottomRef: set it before each scrollToIndex
bottom-scroll, suppress the first intermediate false event (clearing
the guard immediately), so the next event — the corrected position or
a real user scroll — is processed normally.
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