Skip to content

refactor: speed up option selection state in useSelect#4611

Open
TrevorBurnham wants to merge 3 commits into
cloudscape-design:mainfrom
TrevorBurnham:perf/select-selection-lookups
Open

refactor: speed up option selection state in useSelect#4611
TrevorBurnham wants to merge 3 commits into
cloudscape-design:mainfrom
TrevorBurnham:perf/select-selection-lookups

Conversation

@TrevorBurnham

Copy link
Copy Markdown
Contributor

This PR speeds up the selection-state computation that runs on every render in useSelect, which backs both Select and Multiselect.

Previously, useSelect recomputed which options are selected using two nested scans that grew quadratically with the size of the options and selection lists:

  • connectOptionsByValue scanned all dropdown options to find a match for each selected option — O(selected × options).
  • getOptionProps then ran up to three __selectedOptions.indexOf() scans per rendered option (the option itself plus its two neighbours, used to compute selected / isNextSelected / isPreviousSelected) — O(filtered × selected). On the default, non-virtualized PlainList this scans the entire list, so the cost is not bounded by the visible window.

For a large multiselect, this adds up to hundreds of microseconds of avoidable work on every render.

This change makes both lookups linear:

  • connectOptionsByValue builds a Map of dropdown options keyed by value once, then resolves each selected option in O(1)O(selected + options).
  • useSelect builds a Set of the selected dropdown options once per render and tests membership in O(1) per option, replacing the per-option indexOf scans.

The Set is keyed by the same DropdownOption references that connectOptionsByValue returns, so Set.has() is exactly equivalent to the previous reference-based indexOf() > -1 checks — including the undefined-neighbour case at list boundaries.

Benchmarks (Node, isolated microbenchmarks with an equivalence check):

Function Inputs Before After Speedup
connectOptionsByValue 1000 options, 50 selected 147µs 13µs ~11x
connectOptionsByValue 2000 options, 200 selected 993µs 27µs ~37x
getOptionProps (over all options) 1000 options, 100 selected 50µs 12µs ~4x

How has this been tested?

  • New unit tests in src/select/__tests__/connect-options.test.ts cover the edge cases that must be preserved: empty selection, order preservation, parent options skipped, first-match-wins on duplicate values, and matching options with an undefined value.
  • The selection-state change in getOptionProps is covered by the existing src/select/__tests__/use-select.test.ts assertions on selected / isNextSelected / isPreviousSelected, including the group-neighbour case.
  • Full Select, Multiselect, and Autoselect unit suites pass (src/select/, src/multiselect/, src/autosuggest/ — 552 tests green).

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

useSelect recomputed selection state on every Select/Multiselect render
with two nested scans that both grew quadratically with large lists:

- connectOptionsByValue scanned all dropdown options for each selected
  option, an O(selected × options) lookup.
- getOptionProps then ran up to three __selectedOptions.indexOf() scans per
  rendered option (the option plus its two neighbours), O(filtered ×
  selected). On the default non-virtualized PlainList this scans the whole
  list.

Index the dropdown options by value in a Map (O(selected + options)) and
test selection membership through a Set built once per render (O(1) per
option). The Set is keyed by the same DropdownOption references
connectOptionsByValue returns, so Set.has() is exactly equivalent to the
previous reference-based indexOf() > -1 checks, including the
undefined-neighbour case at list boundaries. Benchmarks (Node):

  connectOptionsByValue, 2000 options, 200 selected:  993µs -> 27µs (~37x)
  getOptionProps,        1000 options, 100 selected:   50µs -> 12µs  (~4x)

Behaviour is unchanged: the first non-parent option for a given value still
wins, and empty selections, undefined values, and duplicate values are
preserved. Existing use-select selection assertions (selected /
isNextSelected / isPreviousSelected, incl. the group-neighbour case) plus
new connect-options unit tests cover the behaviour.
@TrevorBurnham TrevorBurnham requested a review from a team as a code owner June 12, 2026 02:48
@TrevorBurnham TrevorBurnham requested review from gethinwebster and removed request for a team June 12, 2026 02:48

@gethinwebster gethinwebster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good but please remove the inline comments, they're adding unnecessary explanations.

Comment thread src/select/utils/connect-options.ts Outdated
Comment thread src/select/utils/use-select.ts Outdated
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.50%. Comparing base (55c9250) to head (e433965).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4611   +/-   ##
=======================================
  Coverage   97.50%   97.50%           
=======================================
  Files         948      948           
  Lines       30314    30317    +3     
  Branches    11046    11047    +1     
=======================================
+ Hits        29559    29562    +3     
+ Misses        748      708   -40     
- Partials        7       47   +40     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

TrevorBurnham and others added 2 commits June 12, 2026 08:29
Co-authored-by: Gethin Webster <gethinw@amazon.de>
Co-authored-by: Gethin Webster <gethinw@amazon.de>
@gethinwebster gethinwebster enabled auto-merge June 12, 2026 12:32
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.

2 participants