Skip to content

perf(Autocomplete): eliminate unnecessary re-renders on arrow-key navigation#7549

Open
hectahertz wants to merge 3 commits intomainfrom
hectahertz/perf-autocomplete-memo-deps
Open

perf(Autocomplete): eliminate unnecessary re-renders on arrow-key navigation#7549
hectahertz wants to merge 3 commits intomainfrom
hectahertz/perf-autocomplete-memo-deps

Conversation

@hectahertz
Copy link
Contributor

@hectahertz hectahertz commented Feb 14, 2026

Closes #

The active highlight in Autocomplete was already applied via DOM by useFocusZone (which sets data-is-active-descendant, styled by ActionList CSS). However, AutocompleteMenu also tracked the highlighted item as React state, causing a full re-render of all 1000+ items on every arrow-key press even though the visual highlight was already handled.

Three changes:

  1. Remove highlightedItem from useMemo dependencies. The selectableItems and allItemsToRender memos depended on highlightedItem, causing the entire item mapping/filtering/sorting pipeline to re-run on every arrow-key press.

  2. Replace highlightedItem state with a ref. setHighlightedItem() triggered a React re-render of the entire menu on every arrow-key press. Since the visual highlight is handled by useFocusZone via DOM, we only need the highlighted item data for autocomplete suggestion logic, which can read from a ref.

  3. Wrap items in React.memo. For re-renders that do happen (e.g. typing to filter), unchanged items skip rendering.

Visual change: active prop removed from keyboard-highlighted items

The old code passed active: highlightedItem?.id === selectableItem.id to each ActionList.Item, applying [data-active] CSS which included semibold font-weight plus a color override, in addition to the background highlight and blue accent line.

Since the keyboard highlight is already styled via [data-is-active-descendant] (set by useFocusZone), the active prop was redundant for background/accent but was also applying bold text that data-is-active-descendant does not.

This is the correct behavior. The [data-active] style (bold text, color override) is for "current page/selection" state (aria-current), not transient keyboard navigation. Autocomplete was the only component applying bold text during keyboard navigation. After this PR, it matches SelectPanel and FilteredActionList.

Comparison across all components using active-descendant CSS
Component Focus pattern Attribute Font weight Blue accent Background
NavList (current page) aria-current data-active 600 (semibold) Yes Yes
SelectPanel (keyboard nav) active-descendant data-is-active-descendant 400 (normal) Yes Yes
FilteredActionList (keyboard nav) active-descendant data-is-active-descendant 400 (normal) Yes Yes
ActionMenu (keyboard nav) real DOM focus :focus-visible 400 (normal) No No (outline)
Autocomplete before active-descendant + active prop data-is-active-descendant + data-active 600 (semibold) Yes Yes
Autocomplete after active-descendant data-is-active-descendant 400 (normal) Yes Yes

VRT snapshots need updating to reflect this intentional correction.

Performance measurements

Measured on the default Autocomplete story modified to 1000 items. Interaction: type "i" to filter (all 1000 items match), then press ArrowDown 5 times.

Metric Before (main) After (PR) Delta
INP 202ms ("Needs Improvement") 133ms ("Good") -69ms (-34%)
Processing 141ms 88ms -53ms (-38%)
Presentation 61ms 42ms -19ms (-31%)
Item re-renders per arrow key ~1000 0 -1000 (-100%)

The remaining 88ms processing is from useFocusZone DOM traversal, scrollIntoView, and the autocomplete suggestion state update (which only re-renders the input, not the list).

Related: #7547 addresses a separate forced reflow issue in useScrollFlash, which also affects Autocomplete via FilteredActionList.

Changelog

New

N/A

Changed

  • AutocompleteMenu no longer re-renders on arrow-key navigation. highlightedItem converted from state to a ref since the visual highlight was already handled by useFocusZone via DOM.
  • Autocomplete suggestion logic moved from a useEffect (dependent on highlight state) to the onActiveDescendantChanged callback.
  • Keyboard-highlighted items no longer show bold text. This aligns Autocomplete with SelectPanel and FilteredActionList, which use data-is-active-descendant (normal weight) for keyboard navigation. The bold data-active style is reserved for aria-current (e.g. NavList current page).

Removed

N/A

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

All 23 Autocomplete tests pass.

  1. Open Autocomplete default story
  2. Type to filter items
  3. Use arrow keys to navigate, confirm highlight moves correctly (background + blue accent line)
  4. Select an item, confirm selection works
  5. Type partial text, confirm autocomplete suggestion still appears
  6. Open DevTools Performance tab, record while pressing arrow keys, confirm no React re-renders on keydown
  7. Compare with SelectPanel keyboard navigation, confirm the highlight styling matches (no bold text in either)

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Feb 14, 2026

🦋 Changeset detected

Latest commit: d7c3e90

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 Patch

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
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.

@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 14, 2026
@github-actions github-actions bot temporarily deployed to storybook-preview-7549 February 14, 2026 15:15 Inactive
@hectahertz hectahertz force-pushed the hectahertz/perf-autocomplete-memo-deps branch 3 times, most recently from 757105c to adc970a Compare February 14, 2026 15:24
@hectahertz hectahertz changed the title perf(Autocomplete): remove highlightedItem from useMemo dependencies perf(Autocomplete): eliminate re-renders on arrow-key navigation by applying highlight via DOM Feb 14, 2026
@github-actions github-actions bot temporarily deployed to storybook-preview-7549 February 14, 2026 15:26 Inactive
@hectahertz hectahertz force-pushed the hectahertz/perf-autocomplete-memo-deps branch from adc970a to 95257f6 Compare February 14, 2026 15:33
@github-actions github-actions bot temporarily deployed to storybook-preview-7549 February 14, 2026 15:36 Inactive
@hectahertz hectahertz force-pushed the hectahertz/perf-autocomplete-memo-deps branch from 95257f6 to d128a81 Compare February 14, 2026 15:47
@hectahertz hectahertz changed the title perf(Autocomplete): eliminate re-renders on arrow-key navigation by applying highlight via DOM perf(Autocomplete): eliminate unnecessary re-renders on arrow-key navigation Feb 14, 2026
@github-actions github-actions bot temporarily deployed to storybook-preview-7549 February 14, 2026 15:49 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-7549 February 14, 2026 16:04 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-7549 February 14, 2026 17:57 Inactive
@hectahertz hectahertz added the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Feb 14, 2026
@hectahertz hectahertz marked this pull request as ready for review February 18, 2026 18:23
@hectahertz hectahertz requested review from a team as code owners February 18, 2026 18:23
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

This PR improves AutocompleteMenu performance by preventing expensive React re-renders during keyboard (active-descendant) navigation, relying on useFocusZone’s DOM-driven highlight styling instead of React state.

Changes:

  • Convert the highlighted item from React state to a ref and remove it from memo dependencies to avoid recomputing/remapping items on arrow-key navigation.
  • Add a memoized item wrapper to reduce per-item renders during parent/menu re-renders.
  • Update Playwright VRT snapshots to reflect the intentional styling change (removal of active/bold-on-keyboard-highlight).

Reviewed changes

Copilot reviewed 2 out of 83 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/react/src/Autocomplete/AutocompleteMenu.tsx Removes highlighted-item state updates from the render pipeline; adds a memoized item component and moves suggestion updates into the active-descendant callback/effect.
.changeset/perf-autocomplete-memo-deps.md Adds a patch changeset describing the performance improvement.
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-light-linux.png Updates VRT snapshot for light theme baseline.
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-light-tritanopia-linux.png Updates VRT snapshot for light theme (tritanopia).
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-light-high-contrast-linux.png Updates VRT snapshot for light theme (high contrast).
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-light-colorblind-linux.png Updates VRT snapshot for light theme (colorblind).
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-dark-linux.png Updates VRT snapshot for dark theme baseline.
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-dark-tritanopia-linux.png Updates VRT snapshot for dark theme (tritanopia).
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-dark-high-contrast-linux.png Updates VRT snapshot for dark theme (high contrast).
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-dark-dimmed-linux.png Updates VRT snapshot for dark dimmed theme baseline.
.playwright/snapshots/components/Autocomplete.test.ts-snapshots/Autocomplete-Rendering-The-Menu-Outside-An-Overlay-dark-colorblind-linux.png Updates VRT snapshot for dark theme (colorblind).

Comment on lines +73 to +76
<ActionList.Item
key={(key ?? id) as string | number}
onSelect={() => onAction(item)}
{...itemProps}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

key on the inner ActionList.Item is unnecessary here because it isn’t part of a list at this level (the list keying is handled by the parent allItemsToRender.map). Keeping both can be misleading and suggests the inner key affects reconciliation when it doesn’t.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +60
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const MemoizedAutocompleteItem = React.memo(function MemoizedAutocompleteItem<T extends Record<string, any>>({
item,
}: {
item: T
}) {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

MemoizedAutocompleteItem is typed as T extends Record<string, any>, but the implementation assumes specific fields exist (id, onAction, leadingVisual, etc.). Using a concrete type (e.g. AutocompleteMenuItem plus the augmented fields added in selectableItems) would prevent accidental misuse and avoid the any escape hatch.

Copilot uses AI. Check for mistakes.
Comment on lines 371 to 375
}
},
},
[loading],
)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

useFocusZone is only re-initialized when the dependencies array changes, and the onActiveDescendantChanged callback now depends on deferredInputValue and selectedItemIds (for suggestion updates). With [loading] as the only dependency, the callback can become stale and compute suggestions using outdated input/selection state. Include the relevant values in the dependencies array, or read them from refs that are kept up to date each render to avoid reinitializing focusZone too often.

Copilot uses AI. Check for mistakes.
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.

1 participant

Comments