Skip to content

perf(css): remove expensive :has() selectors for Safari style-recalc - #8115

Merged
mattcosta7 merged 7 commits into
mainfrom
perf/remove-expensive-has-selectors
Jul 7, 2026
Merged

perf(css): remove expensive :has() selectors for Safari style-recalc#8115
mattcosta7 merged 7 commits into
mainfrom
perf/remove-expensive-has-selectors

Conversation

@mattcosta7

Copy link
Copy Markdown
Contributor

Closes #

Removes several Safari-costly :has() selectors from the CSS, replacing them with cheaper equivalents (data attributes, a sibling selector) or removing dead/no-op rules. :has() can trigger quadratic style-invalidation in WebKit/Safari; this trims the most expensive remaining uses while keeping rendering behavior intact.

Changelog

Changed

  • ActionList/NavList — the collapsed-parent active indicator now keys off the parent <li>'s data-active instead of :has(~ .SubGroup [data-active='true']). NavList already computes this exact state (!isOpen && containsCurrentItem), and the original selector specificity (0,4,0) is preserved.
  • Breadcrumbs — the overflow-menu item is marked with data-overflow-menu in React, replacing :has(.MenuOverlay).
  • SelectPanel — the search clear-action uses input:placeholder-shown ~ .TextInput-action (a forward sibling selector) instead of :has(input:placeholder-shown).

Removed

  • ActionList — dead :has([data-truncate='true']) rule (the data-truncate attribute is never rendered; Truncate emits data-inline), so it matched nothing.
  • SegmentedControl — no-op :focus-within:has(:focus-visible) rule (.Item has no background of its own, so it did nothing).

New

  • NavList — a dev Storybook story (CollapsedSubNavWithCurrentItem) with a play function that collapses an active sub-nav, so the collapsed-parent active state is captured for visual regression (it can't render statically because NavList auto-expands sub-navs containing the current item).

Kept (intentionally)

  • Button:has([data-kbd-chord]) is retained. The keybinding hint arrives as an arbitrary trailingVisual render function, so there's no clean React signal to replace it; a per-button layout-effect querySelector would be worse (and conflicts with reducing querySelector-in-effects). Its anchor is fully static, so it doesn't trigger the dynamic quadratic re-invalidation :has() is known for.

Are the styles identical?

ChangeIdentical?Why
SelectPanel sibling selector✅ Identicalinput and .TextInput-action are the only such siblings in the wrapper; both target the same single element
Breadcrumbs [data-overflow-menu]✅ IdenticalAttribute is on exactly the <li> that :has(.MenuOverlay) matched
ActionList dead :has([data-truncate])✅ IdenticalRule matched nothing (data-truncate never rendered)
SegmentedControl no-op :has(:focus-visible)✅ Identical.Item has no background; the rule was a no-op
ActionList SubGroup active⚠️ SupersetIdentical for standard NavList usage. It additionally lights the collapsed-parent indicator for custom link components that compute aria-current internally — a small correctness improvement the old selector missed. Please confirm via VRT.

Performance benchmarks

Chromium/Blink, 6× CPU throttle, style-recalc isolated via getComputedStyle (no forced layout), min-of-trials. Blink has :has() invalidation sets that Safari lacks, so real-world Safari gains are larger than these.

SelectorMetric:has()AfterImprovement
ActionList :has(~ .SubGroup [data-active])recalc on characteristic mutation (leaf toggles active)7.8 ms3.3 ms57.7%
ActionList (same)forced full restyle2176 ms1600 ms26.5%
Breadcrumbs :has(.MenuOverlay)forced full restyle771 ms522 ms32.2%
SelectPanel :has(input:placeholder-shown)forced full restyle2649 ms1931 ms27.1%
ActionList dead :has([data-truncate])forced full restyle1649 ms898 ms45.5%
SegmentedControl no-op :has(:focus-visible)forced full restyle1097 ms1082 ms1.4%

SelectPanel is roughly neutral per-keystroke in Blink (single input → :has() already cheap); its clear win is under full restyle and, more importantly, in Safari where the keystroke-dynamic :has() is the pathological case.

Rollout strategy

  • Patch release

Testing & Reviewing

  • Unit suites pass: 138 ActionList/NavList + Breadcrumbs/SegmentedControl/Description/SelectPanel2.
  • stylelint / eslint / prettier clean.
  • VRT focus: the ActionList/NavList collapsed-parent indicator is the only behavior-affecting change — review the new Components/NavList/Dev → Collapsed SubNav With Current Item story. The other four changes are provably byte-identical.

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

Replace Safari-costly :has() selectors with cheaper equivalents:
- ActionList/NavList: :has(~ .SubGroup [data-active]) -> parent li's
data-active (NavList already computes !isOpen && containsCurrentItem);
preserves (0,4,0) specificity. Adds a VRT story for the collapsed-active
parent state (needs a play fn since NavList auto-expands active subnavs).
- Breadcrumbs: :has(.MenuOverlay) -> data-overflow-menu on the menu <li>.
- SelectPanel: :has(input:placeholder-shown) -> input:placeholder-shown ~
.TextInput-action (siblings in the wrapper).
- ActionList: remove dead :has([data-truncate]) (attribute never rendered).
- SegmentedControl: remove no-op :focus-within:has(:focus-visible) (.Item
has no background of its own).
Button :has([data-kbd-chord]) intentionally kept (no clean React signal;
static anchor = low cost).
@changeset-bot

changeset-botBot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a223203

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
NameType
@primer/reactPatch

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

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.

CopilotAI 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 reduces Safari/WebKit style-recalculation cost by removing or replacing several :has() selectors in @primer/react component styles, shifting to cheaper selectors (e.g., data attributes, sibling selectors) and deleting dead/no-op rules. It also adds a dev Storybook interaction to capture the only behavior-affecting case in visual regression.

Changes:

  • Replace costly :has()-based styling in ActionList/NavList, Breadcrumbs, and SelectPanel2 with data-attribute and sibling-selector approaches.
  • Remove dead/no-op :has() rules from ActionList and SegmentedControl.
  • Add a NavList dev Storybook story + play function to exercise collapsed-parent “current item” styling for VRT.
Show a summary per file
FileDescription
packages/react/src/SegmentedControl/SegmentedControl.module.cssRemoves a no-op :has() focus rule to avoid unnecessary selector work.
packages/react/src/NavList/NavList.dev.stories.tsxAdds an interaction story to capture collapsed-parent active indicator behavior in VRT.
packages/react/src/experimental/SelectPanel2/SelectPanel.module.cssReplaces :has() with an input sibling selector to hide the clear action when empty.
packages/react/src/Breadcrumbs/Breadcrumbs.tsxMarks the overflow menu item with data-overflow-menu for cheaper CSS targeting.
packages/react/src/Breadcrumbs/Breadcrumbs.module.cssSwitches overflow-item wrapping rule from :has(.MenuOverlay) to [data-overflow-menu].
packages/react/src/ActionList/ActionList.module.cssKeys collapsed-parent active styling off parent data-active and removes a dead :has([data-truncate]) rule.
.changeset/perf-remove-expensive-has-selectors.mdAdds a patch changeset for the Safari style-recalc performance improvements.

Review details

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

Comment thread.changeset/perf-remove-expensive-has-selectors.md Outdated
@github-actions
github-actionsBottemporarily deployed to storybook-preview-8115 July 7, 2026 01:52 Inactive
@mattcosta7
mattcosta7 marked this pull request as ready for review July 7, 2026 10:53
@mattcosta7
mattcosta7 requested a review from a team as a code ownerJuly 7, 2026 10:53
@mattcosta7
mattcosta7 requested a review from joshblackJuly 7, 2026 10:53
@github-actions
github-actionsBottemporarily deployed to storybook-preview-8115 July 7, 2026 15:21 Inactive
@mattcosta7mattcosta7 added the Canary Release Apply this label when you want CI to create a canary release of the current PR label Jul 7, 2026
@github-actions
github-actionsBot requested a deployment to storybook-preview-8115 July 7, 2026 16:37 Abandoned
@github-actions
github-actionsBottemporarily deployed to storybook-preview-8115 July 7, 2026 16:50 Inactive
@github-actions
github-actionsBottemporarily deployed to storybook-preview-8115 July 7, 2026 17:23 Inactive
@primer-integration

Copy link
Copy Markdown

Integration test results from github/github-ui PR:

Passed CI  Passed
Passed VRT  Passed
Passed Projects  Passed

All checks passed!

@mattcosta7
mattcosta7 added this pull request to the merge queueJul 7, 2026
Merged via the queue into main with commit 07c530aJul 7, 2026
54 checks passed
@mattcosta7
mattcosta7 deleted the perf/remove-expensive-has-selectors branch July 7, 2026 19:24
@primerprimerBot mentioned this pull request Jul 7, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Canary ReleaseApply this label when you want CI to create a canary release of the current PRintegration-tests: recommendedThis 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.

3 participants

@mattcosta7@joshblack