-
Notifications
You must be signed in to change notification settings - Fork 648
perf(Dialog): Replace body:has() with direct class and scope footer selector #7329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…elector - Replace body:has(.Dialog.DisableScroll) with direct body.DialogScrollDisabled class - Scope :has(.Footer) to direct child with > combinator for O(1) lookup Part of #7312
🦋 Changeset detectedLatest commit: 1f1be39 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 |
|
👋 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. Or, apply the |
There was a problem hiding this comment.
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 optimizes Dialog component performance by replacing expensive CSS selectors with more efficient alternatives. The main change replaces body:has(.Dialog.DisableScroll) with a direct class DialogScrollDisabled on the body element, and scopes the footer selector using the direct child combinator (>). These optimizations target INP (Interaction to Next Paint) metrics by reducing style recalculation costs, especially on large pages with thousands of DOM nodes.
Key Changes:
- Add/remove
DialogScrollDisabledclass directly to body element via JavaScript instead of relying on CSS:has()selector - Scope footer selector from
:has(.Footer)to:has(> .Footer)for O(1) lookup instead of full subtree search
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/react/src/Dialog/Dialog.tsx | Adds JavaScript logic to add/remove DialogScrollDisabled class to body element in useEffect with cleanup function |
| packages/react/src/Dialog/Dialog.module.css | Replaces expensive body:has(.Dialog.DisableScroll) selector with body:global(.DialogScrollDisabled) and scopes footer selector to direct child |
| .changeset/perf-dialog-has-selector.md | Documents the performance optimization changes in changeset for release notes |
siddharthkp
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems good on the surface, but because it's visual changes, can you please verify this has no accidental side effects in dotcom?
Orrrr, we can feature flag the change to be safe
@siddharthkp totally onboard with flagging! Is there a good pattern for that when the flag relies on changes to global styles and an eager matching selector? if the old Otherwise i'll pick this up in Jan and do some thorough testing too |
Not yet. But adding a data-attribute is probably the way to go. However, in this case, because you are adding the className targeted by the global style styles, you can check for the feature flag in javascript.
Yeah. That's tricky. Does doing something like this help? I'm guessing if the data-optimised doesn't match, it's not expensive? /* we need to add both of these */
/* equivalent to old selector with default value of flag */
.Dialog[data-optimised="false"]:has(.Footer) {
/* optimised behavior when feature flag is true */
.Dialog[data-optimised="true"]:has(> .Footer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
packages/react/src/Dialog/Dialog.tsx
Outdated
| document.body.removeAttribute('data-dialog-scroll-optimized') | ||
| document.body.removeAttribute('data-dialog-scroll-disabled') | ||
| } else { | ||
| document.body.classList.remove('DialogScrollDisabled') |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the DialogScrollDisabled class added on line 309 has no effect (it's not used in any CSS rules), this removal is also unnecessary and should be deleted along with the addition.
…der level (#7393) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Ok, I think this handles it The flagging is a bit rough, since we need to add an attribute to the body when the flag is enabled (and increment/decrement a count when doing so), but I think that handles it well enough - and expect we should be quickly able to remove that afterwards I removed the footer has query changes for now as well, the actual dialog content should be small enough that has is well enough scoped there as is - really just the body unbounded has is the main issue |
…#7398) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
|
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/9550 |
Summary
Performance optimizations for Dialog to improve INP via a feature flag (
primer_react_css_has_selector_perf).Changes
primer_react_css_has_selector_perfflag to toggle between legacy:has()selector and direct data attribute on bodybody:not([data-dialog-scroll-optimized]):has(.Dialog.DisableScroll), optimized path usesbody[data-dialog-scroll-disabled]data-dialog-scroll-optimizedset atFeatureFlagsprovider level to short-circuit:has()evaluation with O(1) negation checkdata-dialog-scroll-disabledset on body when a dialog mounts (optimized path only)Expected INP Impact
Why this matters
body:has(.Dialog.DisableScroll)is extremely expensive:By using
body[data-dialog-scroll-disabled](a direct attribute selector), we eliminate all traversal cost.Why we didn't scope :has() to portal roots
An alternative considered was scoping the
:has()selector to portal root containers (e.g.,[data-portal-root]:has(.Dialog.DisableScroll)). This was rejected because:registerPortalRootsetsdata-portal-rooton portal containers, but scoping:has()to these elements still requires traversing all descendants to find matching elements:has()traversal regardless of the scoping ancestor; the selector must evaluate all descendantsbody[data-dialog-scroll-disabled]is a simple attribute lookup with constant time complexity, regardless of DOM sizeThe only way to achieve true O(1) performance is to avoid
:has()entirely and use a direct attribute on body.Why we kept :has() for footer detection
The
.Dialog:has(.Footer)selector was considered for optimization but left unchanged because::has()scope is limited to the dialog element itself, which contains a small, bounded number of childrenImplementation Details
body:not([data-dialog-scroll-optimized]):has(.Dialog.DisableScroll)selectorbody[data-dialog-scroll-disabled]direct attribute selectordata-dialog-scroll-optimizedis set on body by theFeatureFlagsprovider, causing the:not()check to fail first (O(1)), preventing the expensive:has()evaluation.