Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/perf-basestyles-has-selector-feature-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@primer/react': patch
---

perf(BaseStyles): Feature-flag expensive :has([data-color-mode]) selectors

Add a feature flag (`primer_react_css_perf_has_selector`) to opt-in to skipping expensive `:has([data-color-mode])` selectors that scan the entire DOM on every style recalculation.

To enable the optimization, set the `primer_react_css_perf_has_selector` feature flag to `true` via the `FeatureFlags` component. The BaseStyles component will automatically add the `data-primer-css-perf-has-selector` attribute when the flag is enabled. Input color-scheme is already handled by global selectors in the codebase.

See #7325 and #7312 for context on this performance optimization.
23 changes: 19 additions & 4 deletions packages/react/src/BaseStyles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,30 @@ details-dialog:focus:not(:focus-visible):not(:global(.focus-visible)) {
/* stylelint-disable-next-line primer/colors */
color: var(--BaseStyles-fgColor, var(--fgColor-default));

/* Global styles for light mode */
&:has([data-color-mode='light']) {
/*
* PERFORMANCE: The :has([data-color-mode]) selectors below are expensive
* as they scan the entire DOM on every style recalculation.
* Input color-scheme is already handled by global selectors above:
* [data-color-mode='light'] input { color-scheme: light; }
* [data-color-mode='dark'] input { color-scheme: dark; }
*
* Feature flag: When the primer_react_css_perf_has_selector feature flag
* is disabled (default), the old (expensive) behavior is preserved.
* Enable the feature flag via FeatureFlags to opt-in to the optimized
* behavior (which skips these expensive selectors).
*
* See #7325 and #7312 for context on this performance optimization.
*/

/* Global styles for light mode - only apply when feature flag is NOT present */
&:not([data-primer-css-perf-has-selector]):has([data-color-mode='light']) {
input & {
color-scheme: light;
}
}

/* Global styles for dark mode */
&:has([data-color-mode='dark']) {
/* Global styles for dark mode - only apply when feature flag is NOT present */
&:not([data-primer-css-perf-has-selector]):has([data-color-mode='dark']) {
input & {
color-scheme: dark;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/BaseStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {type CSSProperties, type PropsWithChildren, type JSX} from 'react'
import {clsx} from 'clsx'

import classes from './BaseStyles.module.css'
import {useFeatureFlag} from './FeatureFlags'

import 'focus-visible'

Expand All @@ -14,6 +15,7 @@ export type BaseStylesProps = PropsWithChildren & {
color?: string // Fixes `color` ts-error
}
function BaseStyles({children, color, className, as: Component = 'div', style, ...rest}: BaseStylesProps) {
const cssPerfHasSelector = useFeatureFlag('primer_react_css_perf_has_selector')
const newClassName = clsx(classes.BaseStyles, className)
const baseStyles = {
['--BaseStyles-fgColor']: color,
Expand All @@ -23,6 +25,7 @@ function BaseStyles({children, color, className, as: Component = 'div', style, .
<Component
className={newClassName}
data-portal-root
{...(cssPerfHasSelector && {'data-primer-css-perf-has-selector': true})}
style={{
...baseStyles,
...style,
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {FeatureFlagScope} from './FeatureFlagScope'
export const DefaultFeatureFlags = FeatureFlagScope.create({
primer_react_action_list_item_as_button: false,
primer_react_breadcrumbs_overflow_menu: false,
primer_react_css_perf_has_selector: false,
primer_react_overlay_overflow: false,
primer_react_select_panel_fullscreen_on_narrow: false,
primer_react_select_panel_order_selected_at_top: false,
Expand Down
15 changes: 15 additions & 0 deletions packages/react/src/__tests__/BaseStyles.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {describe, expect, it} from 'vitest'
import BaseStyles from '../BaseStyles'
import classes from '../BaseStyles.module.css'
import {implementsClassName} from '../utils/testing'
import {FeatureFlags} from '../FeatureFlags'

describe('BaseStyles', () => {
implementsClassName(BaseStyles, classes.BaseStyles)
Expand Down Expand Up @@ -37,4 +38,18 @@ describe('BaseStyles', () => {
expect(container.children[0]).toHaveClass('test-classname')
expect(container.children[0]).toHaveStyle({margin: '10px'})
})

it('does not add data-primer-css-perf-has-selector by default', () => {
const {container} = render(<BaseStyles>Hello</BaseStyles>)
expect(container.children[0]).not.toHaveAttribute('data-primer-css-perf-has-selector')
})

it('adds data-primer-css-perf-has-selector when feature flag is enabled', () => {
const {container} = render(
<FeatureFlags flags={{primer_react_css_perf_has_selector: true}}>
<BaseStyles>Hello</BaseStyles>
</FeatureFlags>,
)
expect(container.children[0]).toHaveAttribute('data-primer-css-perf-has-selector')
})
})