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?

Change Identical? Why
SelectPanel sibling selector ✅ Identical input and .TextInput-action are the only such siblings in the wrapper; both target the same single element
Breadcrumbs [data-overflow-menu] ✅ Identical Attribute is on exactly the <li> that :has(.MenuOverlay) matched
ActionList dead :has([data-truncate]) ✅ Identical Rule 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 ⚠️ Superset Identical 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.

Selector Metric :has() After Improvement
ActionList :has(~ .SubGroup [data-active]) recalc on characteristic mutation (leaf toggles active) 7.8 ms 3.3 ms 57.7%
ActionList (same) forced full restyle 2176 ms 1600 ms 26.5%
Breadcrumbs :has(.MenuOverlay) forced full restyle 771 ms 522 ms 32.2%
SelectPanel :has(input:placeholder-shown) forced full restyle 2649 ms 1931 ms 27.1%
ActionList dead :has([data-truncate]) forced full restyle 1649 ms 898 ms 45.5%
SegmentedControl no-op :has(:focus-visible) forced full restyle 1097 ms 1082 ms 1.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-bot Bot 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
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.

@github-actions github-actions Bot requested a deployment to storybook-preview-8115 July 6, 2026 15:20 Abandoned
@github-actions github-actions Bot temporarily deployed to storybook-preview-8115 July 6, 2026 15:31 Inactive
@mattcosta7 mattcosta7 requested a review from Copilot July 6, 2026 21:36

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 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
File Description
packages/react/src/SegmentedControl/SegmentedControl.module.css Removes a no-op :has() focus rule to avoid unnecessary selector work.
packages/react/src/NavList/NavList.dev.stories.tsx Adds an interaction story to capture collapsed-parent active indicator behavior in VRT.
packages/react/src/experimental/SelectPanel2/SelectPanel.module.css Replaces :has() with an input sibling selector to hide the clear action when empty.
packages/react/src/Breadcrumbs/Breadcrumbs.tsx Marks the overflow menu item with data-overflow-menu for cheaper CSS targeting.
packages/react/src/Breadcrumbs/Breadcrumbs.module.css Switches overflow-item wrapping rule from :has(.MenuOverlay) to [data-overflow-menu].
packages/react/src/ActionList/ActionList.module.css Keys collapsed-parent active styling off parent data-active and removes a dead :has([data-truncate]) rule.
.changeset/perf-remove-expensive-has-selectors.md Adds 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-actions Bot temporarily 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 owner July 7, 2026 10:53
@mattcosta7 mattcosta7 requested a review from joshblack July 7, 2026 10:53
@github-actions github-actions Bot temporarily deployed to storybook-preview-8115 July 7, 2026 15:21 Inactive
@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
@github-actions github-actions Bot requested a deployment to storybook-preview-8115 July 7, 2026 16:37 Abandoned
@github-actions github-actions Bot temporarily deployed to storybook-preview-8115 July 7, 2026 16:50 Inactive
@github-actions github-actions Bot temporarily 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 queue Jul 7, 2026
Merged via the queue into main with commit 07c530a Jul 7, 2026
54 checks passed
@mattcosta7 mattcosta7 deleted the perf/remove-expensive-has-selectors branch July 7, 2026 19:24
@primer primer Bot mentioned this pull request Jul 7, 2026
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: 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.

3 participants