Summary
The key holder table currently renders all rows in the DOM. A creator with thousands of holders causes severe layout thrash, high memory usage, and janky scrolling. This issue implements a fully virtualised list that renders only the rows visible in the viewport plus a small overscan buffer, recycles DOM nodes as the user scrolls, and maintains smooth 60fps scrolling for lists of 10,000+ holders.
Scope
1. Virtual list engine (no external library)
- Implement a
useVirtualList hook accepting { itemCount, itemHeight, containerHeight, overscan }
- Returns
{ startIndex, endIndex, offsetY, totalHeight } updated on every scroll event
- Use
IntersectionObserver to detect when the list container enters and exits the viewport and pause scroll event listeners when off-screen
- Scroll event listener must be passive and throttled to one update per
requestAnimationFrame
2. Fixed-height row rendering
- Each holder row has a fixed height of 48px
- The list container has
position: relative with height: totalHeight
- Visible rows are absolutely positioned with
top: index * itemHeight
- Only
endIndex - startIndex + 2 * overscan DOM nodes exist at any time regardless of total holder count
3. Cursor-based data fetching
- Fetch holders in pages of 50 from the key holder list endpoint using React Query's
useInfiniteQuery
- When
endIndex approaches within 20 rows of the last fetched item, trigger the next page fetch
- While a page is loading, render skeleton rows in the overscan zone
- Maintain a flat
Map<index, HolderRow> cache so fetched rows are never re-fetched during the same session
4. Dynamic rank and share recalculation
- When a new page is loaded and the total holder count changes, recompute ranks and share percentages for all already-loaded rows without re-fetching them
- Recomputation must complete in under 5ms for 10,000 rows (use a typed
Float64Array for share percentages)
5. Scroll restoration
- Store the scroll offset in sessionStorage keyed by
holder-list:{creatorWallet}
- On mount, restore the scroll offset and pre-fetch the page containing the restored index before rendering
6. Performance and unit tests
- Performance test: render a 10,000-row list, scroll from top to bottom in 100 steps — assert the maximum DOM node count never exceeds
(containerHeight / itemHeight) + 2 * overscan + 5
- Performance test: 100 scroll events processed in under 16ms total
- Unit tests:
useVirtualList returns correct startIndex and endIndex for various scroll positions
Acceptance Criteria
ETA: 24 hours
Coordinate on Telegram
Summary
The key holder table currently renders all rows in the DOM. A creator with thousands of holders causes severe layout thrash, high memory usage, and janky scrolling. This issue implements a fully virtualised list that renders only the rows visible in the viewport plus a small overscan buffer, recycles DOM nodes as the user scrolls, and maintains smooth 60fps scrolling for lists of 10,000+ holders.
Scope
1. Virtual list engine (no external library)
useVirtualListhook accepting{ itemCount, itemHeight, containerHeight, overscan }{ startIndex, endIndex, offsetY, totalHeight }updated on every scroll eventIntersectionObserverto detect when the list container enters and exits the viewport and pause scroll event listeners when off-screenrequestAnimationFrame2. Fixed-height row rendering
position: relativewithheight: totalHeighttop: index * itemHeightendIndex - startIndex + 2 * overscanDOM nodes exist at any time regardless of total holder count3. Cursor-based data fetching
useInfiniteQueryendIndexapproaches within 20 rows of the last fetched item, trigger the next page fetchMap<index, HolderRow>cache so fetched rows are never re-fetched during the same session4. Dynamic rank and share recalculation
Float64Arrayfor share percentages)5. Scroll restoration
holder-list:{creatorWallet}6. Performance and unit tests
(containerHeight / itemHeight) + 2 * overscan + 5useVirtualListreturns correctstartIndexandendIndexfor various scroll positionsAcceptance Criteria
ETA: 24 hours
Coordinate on Telegram