perf(SelectPanel): Built-in client-side list virtualization via virtualized prop#7531
perf(SelectPanel): Built-in client-side list virtualization via virtualized prop#7531hectahertz wants to merge 12 commits intomainfrom
virtualized prop#7531Conversation
…virtualized` prop Add a `virtualized` boolean prop to SelectPanel/FilteredActionList that enables client-side list virtualization using @tanstack/react-virtual (already a dependency). When enabled, only the visible items plus a small overscan buffer are rendered in the DOM, dramatically improving performance for large lists. - Add `virtualized` prop to FilteredActionListProps with JSDoc - Wire up useVirtualizer with scroll container, dynamic measurement, overscan=10 - Render virtual items with absolute positioning inside sized container - Handle focus zone scroll-to-index for keyboard navigation of virtual items - Thread `virtualized` prop through SelectPanel to FilteredActionList - Add VirtualizedBuiltIn comparison story (side-by-side with timing) - Rename existing consumer-side virtualization story for clarity
🦋 Changeset detectedLatest commit: 7584e24 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in virtualized prop to SelectPanel / FilteredActionList to enable client-side list virtualization (via @tanstack/react-virtual) for large datasets, plus Storybook examples to compare performance.
Changes:
- Plumbs a new
virtualizedprop throughSelectPaneltoFilteredActionList. - Implements virtualization in
FilteredActionListusinguseVirtualizerand absolute-positioned list items. - Updates Storybook examples: renames the existing consumer-managed virtualization story and adds a built-in virtualization comparison story.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/react/src/SelectPanel/SelectPanel.tsx | Forwards the new virtualized prop to FilteredActionList. |
| packages/react/src/SelectPanel/SelectPanel.examples.stories.tsx | Renames the existing virtualization story and adds a new side-by-side “built-in virtualization” story. |
| packages/react/src/FilteredActionList/FilteredActionList.tsx | Introduces the virtualized prop and the core @tanstack/react-virtual integration. |
virtualized propvirtualized prop
|
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/13534 |
francinelucca
left a comment
There was a problem hiding this comment.
🧑🍳 💋. Some non-blocking comments/questions, but otherwise ![]()
The previous 49px estimate caused a 53% overestimate of total virtual height (~88k px vs actual ~57.6k px for 1800 items). Since items are dynamically measured via measureElement, this only affects the initial scroll thumb size and reduces layout shift when jumping to far indices.
- Update virtualized JSDoc to note the limitation - Add __DEV__ console.warn when both virtualized and groupMetadata are set
The useFocusZone callback references virtualizer.range and virtualizer.scrollToIndex. While this worked at runtime (closures capture bindings, not values), having the declaration after its reference was confusing and flagged by reviewers. Moving it above eliminates the apparent TDZ issue.
When virtualized=true and groupMetadata is provided, the grouped content renders normally but the ActionList wrapper was still receiving virtualization styles (position: relative, huge height) and focusOutBehavior was forced to 'stop'. This caused incorrect layout. Now all runtime virtualization logic uses: This ensures grouped lists are never treated as virtualized.
Virtualization-critical styles (height, position, width) must not be overridable by actionListProps.style. Spread consumer styles first so the required virtualization properties always win.
…zed list Absolutely positioned items with width: 100% resolved to the containing block (the position: relative ActionList), ignoring the vertical scrollbar's ~8px. This caused every item to overflow horizontally. Replace with left: 0 + right: 0 on items, and remove width: 100% from the ActionList container. Items now size to 304px (matching non-virtualized behavior) and the horizontal scrollbar is gone.
|
@TylerJDev @siddharthkp Could I get more eyes on this one before we merge it? :) |
See https://github.com/github/primer/discussions/6407
Overview
Adds a
virtualizedboolean prop toSelectPanel(andFilteredActionList) that enables client-side list virtualization using@tanstack/react-virtual(already a dependency).When enabled, only the visible items plus a small overscan buffer are rendered in the DOM. This is a purely client-side optimization — it does not require server-side pagination or API changes. The consumer can still pass all items at once.
Usage
Performance measurements (1,800 items)
Measured using Chrome DevTools Performance traces and PerformanceObserver API on the
VirtualizedBuiltInstory:Screen.Recording.2026-02-12.at.12.36.52.mov
Open time
[role="option"]elementsFiltering (typing "Item" with panel open)
Changelog
New
virtualizedprop onSelectPanelandFilteredActionList— enables client-side list virtualizationVirtualizedBuiltInstory — side-by-side comparison with open-time measurementChanged
VirtualizedConsumerSidefor clarityRemoved
Rollout strategy
Testing & Reviewing
VirtualizedBuiltInstory in Storybook (Components/SelectPanel/Examples)Implementation details
useVirtualizerfrom@tanstack/react-virtualinsideFilteredActionListscrollContainerReffor the virtualizer scroll elementestimateSize: 49pxwith dynamicmeasureElementfor accurate variable-height itemsoverscan: 10items beyond the viewport for smooth scrollingfocusOutBehavior: "stop"when virtualized to prevent focus wrapping past virtual boundariesscrollToIndexin the focus zone when keyboard focus moves to an item outside the visible rangegroupMetadata) are not virtualized — they are typically small enough not to need itMerge checklist