Skip to content

Conversation

@hectahertz
Copy link
Contributor

@hectahertz hectahertz commented Feb 12, 2026

See https://github.com/github/primer/discussions/6407

Overview

Adds a virtualized boolean prop to SelectPanel (and 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. 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

<SelectPanel
  items={items}
  virtualized  // ← that's it
  // ... everything else unchanged
/>

Performance measurements (1,800 items)

Measured using Chrome DevTools Performance traces and PerformanceObserver API on the VirtualizedBuiltIn story:

Screen.Recording.2026-02-12.at.12.36.52.mov

Open time

Metric Without virtualization With virtualization Improvement
Time to open 521 ms 10.6 ms ~49x faster
DOM nodes (total) 20,072 482 97.6% reduction
[role="option"] elements 1,800 19 98.9% reduction
Worst Long Animation Frame 1,192 ms (1,139 ms blocking) None Eliminated

Filtering (typing "Item" with panel open)

Metric Without virtualization With virtualization Improvement
INP 883 ms (Bad) 58 ms (Good) ~15x faster
Worst keypress duration 376 ms 56 ms ~7x faster
Worst Long Animation Frame 1,219 ms None Eliminated
DOM nodes during filtering ~20,000 ~504 97.5% reduction

Changelog

New

  • virtualized prop on SelectPanel and FilteredActionList — enables client-side list virtualization
  • VirtualizedBuiltIn story — side-by-side comparison with open-time measurement

Changed

  • Renamed existing consumer-side virtualization story to VirtualizedConsumerSide for clarity

Removed

  • Nothing

Rollout strategy

  • Minor release

Testing & Reviewing

  1. Open the VirtualizedBuiltIn story in Storybook (Components/SelectPanel/Examples)
  2. Click each "Select labels" button and compare the displayed open times
  3. Type into the filter field of each panel — the virtualized panel should feel instant while the non-virtualized one has noticeable lag
  4. Scroll through the virtualized list to verify smooth scrolling and item measurement
  5. Use keyboard navigation (arrow keys) to verify focus management works correctly with virtualized items

Implementation details

  • Uses useVirtualizer from @tanstack/react-virtual inside FilteredActionList
  • Leverages the existing scrollContainerRef for the virtualizer scroll element
  • Sets estimateSize: 49px with dynamic measureElement for accurate variable-height items
  • overscan: 10 items beyond the viewport for smooth scrolling
  • Automatically sets focusOutBehavior: "stop" when virtualized to prevent focus wrapping past virtual boundaries
  • Handles scrollToIndex in the focus zone when keyboard focus moves to an item outside the visible range
  • Grouped lists (groupMetadata) are not virtualized — they are typically small enough not to need it

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook)
  • Changes are SSR compatible
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge
  • (GitHub staff only) Integration tests pass at github/github

…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
@hectahertz hectahertz requested a review from a team as a code owner February 12, 2026 11:36
@changeset-bot
Copy link

changeset-bot bot commented Feb 12, 2026

🦋 Changeset detected

Latest commit: bf26e4b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

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

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Feb 12, 2026
@github-actions
Copy link
Contributor

👋 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 integration-tests: skipped manually label to skip these checks.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 virtualized prop through SelectPanel to FilteredActionList.
  • Implements virtualization in FilteredActionList using useVirtualizer and 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.

@hectahertz hectahertz changed the title feat(SelectPanel): Built-in client-side list virtualization via virtualized prop perf(SelectPanel): Built-in client-side list virtualization via virtualized prop Feb 12, 2026
@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/13534

Copy link
Member

@francinelucca francinelucca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧑‍🍳 💋. Some non-blocking comments/questions, but otherwise :shipit:

*
* @default false
*/
virtualized?: boolean
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:

  • add to SelectPanel.docs.json

containerRef: {current: listContainerElement},
bindKeys: FocusKeys.ArrowVertical | FocusKeys.PageUpDown,
focusOutBehavior,
focusOutBehavior: virtualized ? 'stop' : focusOutBehavior,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why this is needed 👀

Comment on lines +400 to +413
if (groupMetadata?.length) {
return groupMetadata.map((group, index) => {
if (index === firstGroupIndex && getItemListForEachGroup(group.groupId).length === 0) {
firstGroupIndex++
}
return (
<ActionList.Group key={index}>
<ActionList.GroupHeading variant={group.header?.variant ? group.header.variant : undefined}>
{group.header?.title ? group.header.title : `Group ${group.groupId}`}
</ActionList.GroupHeading>
{getItemListForEachGroup(group.groupId).map(({key: itemKey, ...item}, itemIndex) => {
const key = itemKey ?? item.id?.toString() ?? itemIndex.toString()
return (
<MappedActionListItem
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean the virtualized option won't work with groups? if so we should call that out in the prop info

? {
height: virtualizer.getTotalSize(),
width: '100%',
position: 'relative' as const,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do the 'height' and 'width' SelectPanel props still work alongside these?

@primer-integration
Copy link

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants