diff --git a/.changeset/perf-basestyles-has-selector-feature-flag.md b/.changeset/perf-basestyles-has-selector-feature-flag.md new file mode 100644 index 00000000000..53b4dd93fd5 --- /dev/null +++ b/.changeset/perf-basestyles-has-selector-feature-flag.md @@ -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. diff --git a/packages/react/src/BaseStyles.module.css b/packages/react/src/BaseStyles.module.css index 76259ac05dd..8eb23fffcba 100644 --- a/packages/react/src/BaseStyles.module.css +++ b/packages/react/src/BaseStyles.module.css @@ -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; } diff --git a/packages/react/src/BaseStyles.tsx b/packages/react/src/BaseStyles.tsx index 4631ede237a..780adf6dbe5 100644 --- a/packages/react/src/BaseStyles.tsx +++ b/packages/react/src/BaseStyles.tsx @@ -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' @@ -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, @@ -23,6 +25,7 @@ function BaseStyles({children, color, className, as: Component = 'div', style, . { implementsClassName(BaseStyles, classes.BaseStyles) @@ -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(Hello) + 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( + + Hello + , + ) + expect(container.children[0]).toHaveAttribute('data-primer-css-perf-has-selector') + }) })