Skip to content

perf(ActionList): replace :has() selector with JS-computed attribute#7714

Open
hectahertz wants to merge 4 commits intomainfrom
hectahertz/perf-action-list-has-selector
Open

perf(ActionList): replace :has() selector with JS-computed attribute#7714
hectahertz wants to merge 4 commits intomainfrom
hectahertz/perf-action-list-has-selector

Conversation

@hectahertz
Copy link
Copy Markdown
Contributor

@hectahertz hectahertz commented Mar 30, 2026

Closes https://github.com/github/github-ui/issues/17359

This CSS selector on the ActionList container forces the browser to walk the entire subtree on every DOM mutation:

&:has([data-has-description='true']):has([data-has-description='false']) {
  & .ItemLabel { font-weight: var(--base-text-weight-normal); }
}

With 100 items (~1860 DOM elements), sequential DOM mutations with forced style recalc (the pattern during React reconciliation):

Metric Before (:has()) After (data attr) Improvement
Remove all items 426ms 10ms 42x faster
Add all items 415ms 27ms 15x faster
Total 841ms 37ms 23x faster
Per mutation 4.2ms 0.18ms 23x faster

(Chrome, 100 items, 6x CPU throttle. Safari is worse due to historically quadratic :has() invalidation, causing the 10-20+ second freezes reported in github/github-ui#17223.)

Replaced with a data-mixed-descriptions attribute computed via useLayoutEffect in List.tsx. Two querySelector calls after render replace continuous full-subtree style invalidation from the CSS engine.

Why DOM queries instead of React children/context:

  • Each Item's description is detected via useSlots at render time, so the List can't know which Items have descriptions without duplicating slot detection or deeply inspecting opaque children trees (fragile with Groups, conditional rendering, wrapper components)
  • A context-based approach (Items registering description state) would work but adds registration/unregistration callbacks, a new provider, and re-renders when the count changes. Not worth the complexity for a single derived boolean

Related: #7708 (stylelint guard for container-level :has() selectors), github/github-ui#17223

Changelog

New

N/A

Changed

  • Replaced CSS :has([data-has-description]) container selector with data-mixed-descriptions attribute computed in JS

Removed

N/A

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

  1. Open Storybook, navigate to StressTests/Components/ActionList/MixedDescriptions. Click "Start" to run the stress test
  2. Verify labels use font-weight: normal when the list has mixed items (some with descriptions, some without)
  3. Check an ActionList where all items have descriptions, or none do. Labels should use their default font-weight
  4. For perf validation: open Chrome DevTools Performance tab with 6x CPU throttling, run the stress test. Style recalculation per mutation should be <1ms

Merge checklist

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 30, 2026

⚠️ No Changeset found

Latest commit: 849232e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a 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 Mar 30, 2026
@github-actions
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. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

@hectahertz hectahertz added the skip changeset This change does not need a changelog label Mar 30, 2026
@hectahertz hectahertz marked this pull request as ready for review March 30, 2026 16:27
@hectahertz hectahertz requested a review from a team as a code owner March 30, 2026 16:27
@hectahertz hectahertz requested review from Copilot and jonrohan March 30, 2026 16:27
@primer
Copy link
Copy Markdown
Contributor

primer bot commented Mar 30, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a 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 aims to improve ActionList rendering performance by removing an expensive container-level :has() selector and replacing it with a JS-computed data-mixed-descriptions attribute. However, the PR also introduces a new packages/primer-api workspace plus a new GitHub Actions workflow for a Slack/LLM “Primer Bot”, which significantly expands scope beyond the stated goal.

Changes:

  • ActionList: replace container :has() selector with data-mixed-descriptions computed after render.
  • Add a new @primer/api workspace (HTTP server + GitHub Action entrypoint) for answering Primer questions via LLM + primer.style docs.
  • Add a new primer-bot.yml workflow to run the bot on repository_dispatch / manual dispatch.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
packages/react/src/ActionList/List.tsx Computes and sets data-mixed-descriptions on the list element post-render.
packages/react/src/ActionList/ActionList.module.css Switches styling condition from :has() to [data-mixed-descriptions='true'].
packages/primer-api/tsconfig.json Adds TS config for the new @primer/api workspace.
packages/primer-api/src/prompts.ts Defines the LLM system prompt and user prompt builder.
packages/primer-api/src/llm.ts Adds OpenAI/GitHub Models chat completion integration and context retrieval wiring.
packages/primer-api/src/knowledge.ts Retrieves relevant component docs from primer.style and formats context for the prompt.
packages/primer-api/src/index.ts Implements a small HTTP server with /ask and /health endpoints.
packages/primer-api/src/config.ts Loads configuration from environment variables (tokens, model, port, etc.).
packages/primer-api/src/action.ts GitHub Actions entrypoint to read dispatch payload and post an answer to Slack.
packages/primer-api/package.json Declares the new workspace package, scripts, and dependencies.
packages/primer-api/README.md Documents bot setup (Slack + repo dispatch + workflow) and local dev usage.
packages/primer-api/.env.example Provides example env vars for local usage.
.github/workflows/primer-bot.yml Adds workflow to run the Primer bot from dispatch events or manual input.
package-lock.json Updates lockfile for new workspace/dependencies and version bumps.


Adds dynamic LLM-powered answers to the #primer Slack channel. When someone reacts to a question with :robot_face:, a GitHub Action fires, retrieves relevant Primer docs, generates an answer via GitHub Models, and posts it back as a thread reply.

Runs alongside the existing Moveworks-based Primer Bot (which handles @mentions with FAQ matching). No hosted endpoint, no API keys to manage.
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README says “No hosted endpoint, no API keys to manage.”, but the setup requires at least a GitHub Models token/PAT and a Slack bot token (and optionally OpenAI/PRIMER_API_KEY). Consider rephrasing to clarify that there’s no hosted service to operate, but secrets/tokens are still required.

Suggested change
Runs alongside the existing Moveworks-based Primer Bot (which handles @mentions with FAQ matching). No hosted endpoint, no API keys to manage.
Runs alongside the existing Moveworks-based Primer Bot (which handles @mentions with FAQ matching). There’s no separate hosted endpoint or long-lived service to operate—you just configure the required GitHub and Slack tokens as GitHub Actions secrets (see Setup).

Copilot uses AI. Check for mistakes.
@github-actions github-actions bot temporarily deployed to storybook-preview-7714 March 30, 2026 16:36 Inactive
@hectahertz hectahertz force-pushed the hectahertz/perf-action-list-has-selector branch from a721664 to c383302 Compare March 30, 2026 16:36
@github-actions github-actions bot requested a deployment to storybook-preview-7714 March 30, 2026 16:41 Abandoned
@primer
Copy link
Copy Markdown
Contributor

primer bot commented Mar 30, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

@hectahertz hectahertz force-pushed the hectahertz/perf-action-list-has-selector branch from faeb40f to 0919c3b Compare March 30, 2026 16:48
- Use useIsomorphicLayoutEffect for SSR compatibility
- Only update data-mixed-descriptions when value changes
- Remove attribute when not needed instead of setting 'false'
@github-actions github-actions bot temporarily deployed to storybook-preview-7714 March 30, 2026 16:51 Inactive
@hectahertz hectahertz enabled auto-merge March 30, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm skip changeset This change does not need a changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants