Skip to content

perf(useAnchoredPosition): cache scrollable-ancestor walk per anchor#8116

Open
mattcosta7 wants to merge 9 commits into
mainfrom
perf/anchored-position-cache-scrollables
Open

perf(useAnchoredPosition): cache scrollable-ancestor walk per anchor#8116
mattcosta7 wants to merge 9 commits into
mainfrom
perf/anchored-position-cache-scrollables

Conversation

@mattcosta7

@mattcosta7 mattcosta7 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Closes #

useAnchoredPosition (used by every Overlay, AnchoredOverlay, menu, tooltip, and popover) attaches scroll listeners to the anchor's scrollable ancestors. The setup effect lists updatePosition in its dependencies, so it re-walked the entire ancestor chain — calling getComputedStyle on every ancestor — each time a consumer's reposition dependencies changed, even though the set of scrollable ancestors only depends on the anchor element.

This caches the walk keyed on the anchor element and reuses it when the anchor is unchanged (the common case). Listener attach/detach behavior is unchanged; only the redundant getComputedStyle walk is skipped.

Changelog

Changed

  • useAnchoredPosition caches its scrollable-ancestor walk per anchor element, avoiding repeated getComputedStyle ancestor traversals (and the style recalc they force) when the reposition dependencies change while an overlay is open.

Performance

Each avoided walk (a getComputedStyle per ancestor) costs roughly, at 1× CPU:

Ancestor depth Cost per walk
10 ~4.5 µs
25 ~8 µs
50 ~12.5 µs

The win scales with DOM depth and with how often the overlay's consumer changes reposition dependencies while open; more importantly it removes a forced style recalc from that path during interaction.

Rollout strategy

  • Patch release

Testing & Reviewing

  • 53 tests pass across useAnchoredPosition, Overlay, and AnchoredOverlay suites; eslint/prettier clean.
  • Behavior is preserved: the same scroll listeners are attached/removed on each effect run; only the ancestor DOM walk is memoized by anchor identity.
  • Caveat for reviewers: the cache is keyed on anchor-element identity, so if an ancestor's overflow style changed while the same overlay stayed open, the cached ancestor list could be momentarily stale (the previous code only caught this incidentally too). This is a high-blast-radius core hook — please run the github/github-ui integration suite to confirm overlay/menu/tooltip repositioning on scroll is unaffected.

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

The scroll-listener setup effect lists `updatePosition` in its deps, so it
re-walked the entire ancestor chain (a getComputedStyle per ancestor) every
time a consumer's reposition dependencies changed — even though the set of
scrollable ancestors only depends on the anchor element.

Cache the walk keyed on the anchor element and reuse it when the anchor is
unchanged (the common case). Listener attach/detach behavior is unchanged;
only the redundant getComputedStyle walk is skipped.
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1c3d9b9

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 Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 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. If this doesn't work, you can also use the original workflow here. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

To publish a canary release for integration testing, apply the Canary Release label to this PR.

`handleScroll` only schedules a rAF and never calls preventDefault, so the
scroll listeners attached to the anchor's scrollable ancestors can be passive.
This lets Safari/Chrome scroll without waiting to see if the default is
cancelled, improving scroll smoothness for overlays, menus, and tooltips.
@mattcosta7 mattcosta7 changed the title perf(useAnchoredPosition): cache scrollable-ancestor walk per anchor perf(useAnchoredPosition): cache ancestor walk + passive scroll listeners Jul 6, 2026
@mattcosta7 mattcosta7 changed the title perf(useAnchoredPosition): cache ancestor walk + passive scroll listeners perf(useAnchoredPosition): cache scrollable-ancestor walk per anchor Jul 6, 2026
@github-actions github-actions Bot temporarily deployed to storybook-preview-8116 July 6, 2026 15:46 Inactive
@github-actions github-actions Bot temporarily deployed to storybook-preview-8116 July 6, 2026 15:55 Inactive
@mattcosta7 mattcosta7 marked this pull request as ready for review July 7, 2026 10:54
@mattcosta7 mattcosta7 requested a review from a team as a code owner July 7, 2026 10:54
@mattcosta7 mattcosta7 requested review from Copilot and siddharthkp July 7, 2026 10:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 optimizes useAnchoredPosition by memoizing the scrollable-ancestor traversal (which calls getComputedStyle up the ancestor chain) so the scroll-listener effect can re-run on updatePosition changes without repeating the DOM walk when the anchor is unchanged.

Changes:

  • Cache the result of getScrollableAncestors(anchorEl) across effect re-runs keyed by anchor identity.
  • Add a patch changeset documenting the performance improvement for overlays/menus/tooltips.
Show a summary per file
File Description
packages/react/src/hooks/useAnchoredPosition.ts Adds a ref-backed cache to avoid repeated scrollable-ancestor traversal when the scroll-listener effect re-runs.
.changeset/perf-anchored-position-cache-scrollables.md Patch changeset describing the useAnchoredPosition perf improvement.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread packages/react/src/hooks/useAnchoredPosition.ts
Comment thread packages/react/src/hooks/useAnchoredPosition.ts Outdated
@github-actions github-actions Bot temporarily deployed to storybook-preview-8116 July 7, 2026 15:25 Inactive
@mattcosta7 mattcosta7 self-assigned this Jul 7, 2026
@mattcosta7 mattcosta7 added the Canary Release Apply this label when you want CI to create a canary release of the current PR label Jul 7, 2026
@primer-integration

primer-integration Bot commented Jul 7, 2026

Copy link
Copy Markdown

Integration test results from github/github-ui PR:

Waiting  CI   Waiting
Passed  VRT   Passed
Running  Projects   Running

@mattcosta7 mattcosta7 enabled auto-merge July 7, 2026 19:15
@mattcosta7

Copy link
Copy Markdown
Contributor Author

Integration test results from github/github-ui PR:

Waiting  CI   Waiting Passed  VRT   Passed Running  Projects   Running

these all passed, but aren't reporting right?

@mattcosta7 mattcosta7 added the integration-tests: passing Changes in this PR do NOT cause breaking changes in gh/gh label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, there are new commits since the last successful integration test. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. If this doesn't work, you can also the your original workflow here. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

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

Labels

Canary Release Apply this label when you want CI to create a canary release of the current PR integration-tests: passing Changes in this PR do NOT cause breaking changes in gh/gh 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