perf(PageHeader): replace 18 :has() selectors with hoisted data attributes#7901
perf(PageHeader): replace 18 :has() selectors with hoisted data attributes#7901mattcosta7 wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 8e4a4bd The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
There was a problem hiding this comment.
Pull request overview
This PR improves PageHeader selector performance by replacing subtree-scoped :has() CSS selectors with root-level data attributes derived during render.
Changes:
- Hoists
TitleAreavariant andNavigationpresence/hidden state onto thePageHeaderroot. - Replaces PageHeader
:has()selectors with root attribute selectors. - Adds a patch changeset for
@primer/react.
Show a summary per file
| File | Description |
|---|---|
packages/react/src/PageHeader/PageHeader.tsx |
Adds render-time state hoisting and shared hidden-attribute prefix support. |
packages/react/src/PageHeader/PageHeader.module.css |
Replaces :has() selectors with root data-attribute selectors. |
.changeset/perf-pageheader-has-selectors.md |
Documents the PageHeader performance change as a patch release. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 2
| for (const child of React.Children.toArray(children)) { | ||
| if (!React.isValidElement(child)) continue | ||
| if (child.type === TitleArea) { | ||
| titleVariant = (child.props as TitleAreaProps).variant ?? 'medium' | ||
| } else if (child.type === Navigation) { | ||
| hasNavigation = true | ||
| navigationHidden = (child.props as NavigationProps).hidden ?? false | ||
| } | ||
| } |
|
|
||
| // Hoist title size + navigation visibility off direct children onto the | ||
| // root so styling can use plain attribute selectors instead of `:has()`. | ||
| let titleVariant: TitleAreaProps['variant'] = 'medium' |
Closes #
Replaces 18
:has()selectors inPageHeaderwith plain attribute selectors on the root by hoisting title-size and navigation-visibility state from descendants up to the<PageHeader>root at render time.PageHeaderis a top-of-page component; the cost matters less for invalidation frequency (rare DOM churn) and more for selector-matching breadth: the 12 title-size:has()rules + 4 nav-hidden:has()rules + 2 negation:has()rules all sit on the same subject (the root.PageHeader) and the engine has to re-test all of them on every descendant attribute change inside the header. With this PR, every rule on.PageHeaderbecomes a constant-time attribute lookup on the same element.Approach:
RootwalksReact.Children.toArray(children)once to find the officialTitleAreaandNavigationchildren (bychild.type, the same way the existing dev-warning code matchesContextArea/LeadingAction). It readsvariantandhiddenand emits:data-title-size-variant/data-title-size-variant-{narrow,regular,wide}on the root (mirrors the existingdata-size-variant*onTitleArea).data-has-navon the root when aNavigationchild is present.data-nav-hidden-{all,narrow,regular,wide}on the root, mirroring the existingdata-hidden-*onNavigation.The existing attributes on
TitleAreaandNavigationare kept untouched so any external selectors that rely on them continue to work.The existing
getHiddenDataAttributeshelper grew an optionalprefixparameter so the same logic produces thedata-nav-hidden-*set without duplication.Changelog
New
data-title-size-variant/data-title-size-variant-{narrow,regular,wide}on thePageHeaderroot, mirroringTitleArea'svariantprop.data-has-navon thePageHeaderroot when aPageHeader.Navigationchild is rendered.data-nav-hidden-{all,narrow,regular,wide}on thePageHeaderroot, mirroringPageHeader.Navigation'shiddenprop.Changed
PageHeader.module.cssno longer uses:has()(18 selectors removed).Removed
Nothing public.
Rollout strategy
Testing & Reviewing
PageHeaderunit tests pass.tsc --noEmitonpackages/reactis clean.VRT expectations: No change to rendered output for any usage that follows the documented
PageHeader.TitleArea/PageHeader.NavigationAPI. Edge case: a consumer who renders their own custom element carryingdata-component='TitleArea'(rather than using<PageHeader.TitleArea>) would no longer get the size styles, because the React-children walk matches by component identity. This mirrors the existing dev-warning code inRoot, which already walks direct DOM children to find the TitleArea, so any existing wrapping that broke that warning also wouldn't have had the variant rules apply consistently. No usage in this repo is affected.Merge checklist