Skip to content

perf(SelectPanel): Built-in client-side list virtualization via virtualized prop#7531

Open
hectahertz wants to merge 12 commits intomainfrom
hectahertz/selectpanel-built-in-virtualization
Open

perf(SelectPanel): Built-in client-side list virtualization via virtualized prop#7531
hectahertz wants to merge 12 commits intomainfrom
hectahertz/selectpanel-built-in-virtualization

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: 7584e24

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.

@github-actions github-actions bot temporarily deployed to storybook-preview-7531 February 12, 2026 12:13 Inactive
@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:

@primer-integration
Copy link

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

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
@github-actions github-actions bot requested a deployment to storybook-preview-7531 February 13, 2026 16:03 Abandoned
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.
@github-actions github-actions bot requested a deployment to storybook-preview-7531 February 13, 2026 16:13 Abandoned
…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.
@hectahertz
Copy link
Contributor Author

@TylerJDev @siddharthkp Could I get more eyes on this one before we merge it? :)

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