Skip to content

Comments

perf(TreeView): replace O(n) TreeWalker with O(depth) sibling traversal#7544

Open
hectahertz wants to merge 6 commits intomainfrom
hectahertz/perf-treeview-sibling-traversal
Open

perf(TreeView): replace O(n) TreeWalker with O(depth) sibling traversal#7544
hectahertz wants to merge 6 commits intomainfrom
hectahertz/perf-treeview-sibling-traversal

Conversation

@hectahertz
Copy link
Contributor

@hectahertz hectahertz commented Feb 14, 2026

Closes #

Replaces the O(n) TreeWalker-based navigation in getVisibleElement with O(depth) direct DOM sibling/parent traversal for ArrowUp/ArrowDown keyboard navigation.

The previous implementation created a fresh document.createTreeWalker(root, ...) on every ArrowUp/ArrowDown keystroke, then walked from the first node in the tree to the currently focused element before continuing. At position 25K in a 26K-item tree, this was essentially a full DOM traversal on every keypress.

The new implementation walks sibling and parent/child edges directly:

  • ArrowDown: if expanded, go to first child; else next sibling; if no next sibling, walk up to parent's next sibling. O(depth).
  • ArrowUp: go to previous sibling's deepest last visible descendant; if no previous sibling, go to parent. O(depth).

This relies on TreeView.SubTree unmounting children when collapsed (return null when !isExpanded), so collapsed subtree children are never in the DOM.

Benchmarks (26,000 items, 289K DOM nodes)

Per-operation cost (single call):

Position in tree Old (TreeWalker) New (sibling traversal)
100 0.5ms <0.01ms
2,000 5.1ms <0.01ms
10,000 21.6ms <0.01ms
25,000 55.6ms <0.01ms

Held key simulation (50 rapid ArrowDown from position 12,500):

Metric Old (TreeWalker) New (sibling traversal)
Total time 1,878ms 357ms
Avg per keypress 37.6ms 7.1ms
Bottleneck TreeWalker O(n) traversal scrollIntoView forced reflow (separate issue)

The remaining ~7ms per keypress is dominated by scrollIntoView forcing synchronous layout reflow on 289K DOM nodes, addressed separately in #7545.

Changelog

New

N/A

Changed

  • getVisibleElement in useRovingTabIndex.ts now uses direct DOM sibling/parent traversal instead of TreeWalker, reducing ArrowUp/ArrowDown navigation from O(n) to O(tree depth)

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 51 existing TreeView tests pass unchanged
  • To manually verify: open the StressTest story, expand several directories, and use ArrowUp/ArrowDown to navigate. Focus should move correctly through expanded subtrees and skip collapsed ones.

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

@changeset-bot
Copy link

changeset-bot bot commented Feb 14, 2026

🦋 Changeset detected

Latest commit: 31e823d

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

This PR improves TreeView keyboard navigation performance by replacing TreeWalker-based visible-item lookup with direct DOM sibling/parent traversal, avoiding full-tree scans on every ArrowUp/ArrowDown keypress.

Changes:

  • Replaced getVisibleElement implementation in useRovingTabIndex.ts with O(depth) traversal for next/previous visible treeitems.
  • Updated an ActionList story to keep hook-using components at module scope (React Compiler compatibility).
  • Enabled React Compiler processing for ActionList by removing it from the “unsupported” list and added a patch changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
packages/react/src/TreeView/useRovingTabIndex.ts Reworks ArrowUp/ArrowDown visible navigation to use sibling/parent traversal instead of TreeWalker.
packages/react/src/ActionList/ActionList.features.stories.tsx Moves SideEffectDescription to module scope to satisfy React Compiler constraints in stories.
packages/react/script/react-compiler.mjs Removes ActionList from the unsupported set so it’s included in React Compiler processing.
.changeset/treeview-sibling-traversal.md Adds a patch changeset entry documenting the TreeView perf improvement.

Copy link
Contributor

@liuliu-dev liuliu-dev left a comment

Choose a reason for hiding this comment

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

This looks great! Thanks for the detailed comments which really helped with understanding ❤️

I left some comments but non-blocking.

Also curious, are you considering similar improvements for the O(n) paths in getLastElement , getNextPageElement, and getPreviousPageElement?

- Remove ActionList from the React Compiler unsupported list
- Fix Rules of Hooks violation in ChildWithSideEffects story by
  moving SideEffectDescription to module scope
@hectahertz hectahertz force-pushed the hectahertz/perf-treeview-sibling-traversal branch from a71ef36 to eb73368 Compare February 23, 2026 17:40
@primer
Copy link
Contributor

primer bot commented Feb 23, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

@github-actions github-actions bot temporarily deployed to storybook-preview-7544 February 23, 2026 17:51 Inactive
@primer
Copy link
Contributor

primer bot commented Feb 23, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

@github-actions github-actions bot requested a deployment to storybook-preview-7544 February 23, 2026 17:58 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7544 February 23, 2026 18:09 Inactive
@hectahertz hectahertz enabled auto-merge February 23, 2026 18:12
@primer-integration
Copy link

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

@primer-integration
Copy link

Integration test results from github/github-ui:

Waiting  CI   Waiting
Passed  VRT   Passed
Passed  Projects   Passed

@liuliu-dev
Copy link
Contributor

liuliu-dev commented Feb 24, 2026

I’m seeing this failure in the integration tests:

❌ > nx run @github-ui/nested-list-view:test --runInBand
  
  
  > @github-ui/nested-list-view@1.0.0 test
  > jest --runInBand
  
  PASS src/NestedListItem/__tests__/NestedListItem.test.tsx (24.015 s)
   LOGGING RETRY ERRORS  Nested ListView focus management allows arrow navigation for list items
   RETRY 1 
  
      expect(element).not.toHaveAttribute("data-focus-visible-added") // element.hasAttribute("data-focus-visible-added")
  
      Expected the element not to have attribute:
        data-focus-visible-added
      Received:
        data-focus-visible-added=""
  
        17 |
        18 |     expect(items[0]).toHaveAttribute('data-focus-visible-added')
      > 19 |
           | ^
        20 |     await user.keyboard('[ArrowDown]')
        21 |
        22 |     expect(items[0]).not.toHaveAttribute('data-focus-visible-added')
  
        at Object.toHaveAttribute (src/__tests__/NestedListViewFocusManagement.test.tsx:19:26)

It’s happening across multiple integration test runs for this PR. Could this be related to this change (since it affects tab/focus behavior)?

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