Skip to content

perf(Button): fix CounterLabel remount, remove DEV hook, enable React Compiler#7552

Merged
hectahertz merged 3 commits intomainfrom
hectahertz/perf-button-optimize-render
Feb 18, 2026
Merged

perf(Button): fix CounterLabel remount, remove DEV hook, enable React Compiler#7552
hectahertz merged 3 commits intomainfrom
hectahertz/perf-button-optimize-render

Conversation

@hectahertz
Copy link
Contributor

@hectahertz hectahertz commented Feb 15, 2026

Closes #

Three optimizations for Button, the most frequently used component, plus enabling React Compiler.

1. Fix CounterLabel unmount/remount on every re-render

An inline arrow function was passed as a component type to renderModuleVisual. React saw a new component reference every render, unmounting and remounting the CounterLabel DOM subtree instead of diffing the text content in place. Now passes CounterLabel as a JSX element directly, so isElement() returns true and it renders via identity-stable {Visual} instead of <Visual />.

2. Remove conditional __DEV__ hook

A React.useEffect inside an if (__DEV__) block was the primary reason Button was excluded from React Compiler. Replaced with a synchronous render-time ref check that achieves the same validation without a hook call.

3. Enable React Compiler for Button

With the conditional hook removed, Button is fully compatible with React Compiler. Removed src/Button/**/*.tsx from the exclusion list.

Measurements (500 Buttons with changing count prop)

Metric main This PR Improvement
Re-render time 203ms 181ms 10.8% faster
DOM mutations per re-render 2,001 1,001 50% fewer
DOM node churn (add+remove) 2,000 0 100% eliminated
Added nodes 1,000 0 -1,000
Removed nodes 1,000 0 -1,000

On main, the inline arrow function causes React to remove and reinsert each CounterLabel <span> on every re-render (1,000 DOM operations). With the fix, React diffs the text content in place with zero node churn.

Changelog

New

N/A

Changed

  • CounterLabel passed as JSX element instead of component function, preventing DOM remounts
  • DEV-only element validation no longer uses useEffect
  • Button now compiled by React Compiler

Removed

  • src/Button/**/*.tsx from the React Compiler exclusion list

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. Render <Button count={5}>Click</Button> and verify the counter appears
  2. Re-render the parent with a different count, verify the CounterLabel DOM node is reused (not remounted)
  3. In development, render <Button as="div">test</Button> and verify console warning still appears
  4. Run tests with React Compiler active: npx vitest run packages/react/src/Button/

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook)
  • Changes are SSR compatible
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge
  • (GitHub staff only) Integration tests pass at github/github

@changeset-bot
Copy link

changeset-bot bot commented Feb 15, 2026

🦋 Changeset detected

Latest commit: ac1a16c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

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

@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 Feb 15, 2026
@github-actions
Copy link
Contributor

👋 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 integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot requested a deployment to storybook-preview-7552 February 15, 2026 09:29 Abandoned
@hectahertz hectahertz changed the title perf(Button): fix CounterLabel remount and remove conditional DEV hook perf(Button): fix CounterLabel remount, remove DEV hook, enable React Compiler Feb 15, 2026
@hectahertz hectahertz marked this pull request as ready for review February 16, 2026 14:09
@hectahertz hectahertz requested a review from a team as a code owner February 16, 2026 14:09
Copy link
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

Optimizes Button rendering in @primer/react by preventing unnecessary CounterLabel remounts, removing a DEV-only conditional hook to allow React Compiler optimization, and updating the compiler support list accordingly.

Changes:

  • Pass CounterLabel as a JSX element to renderModuleVisual to avoid unmount/remount churn on re-renders.
  • Replace the DEV-only conditional useEffect with a render-time element validation check.
  • Enable React Compiler for Button by removing src/Button/**/*.tsx from the unsupported list, and add a patch changeset.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/react/src/Button/ButtonBase.tsx Fixes CounterLabel rendering to avoid remounts; changes DEV validation logic.
packages/react/script/react-compiler.mjs Removes Button from React Compiler unsupported set.
.changeset/button-optimize-render.md Adds patch changeset describing the Button perf change.
Comments suppressed due to low confidence (1)

packages/react/src/Button/ButtonBase.tsx:158

  • There doesn’t appear to be unit test coverage for the count/CounterLabel rendering path, and this change is specifically meant to prevent CounterLabel being unmounted/remounted across re-renders. Please add a regression test that renders a Button with count, re-renders with a different count, and asserts the CounterLabel DOM node identity is preserved (no remount) while its text updates.
                count !== undefined && !TrailingVisual
                  ? renderModuleVisual(
                      <CounterLabel className={classes.CounterLabel} data-component="ButtonCounter">
                        {count}
                      </CounterLabel>,
                      Boolean(loading) && !LeadingVisual,
                      'trailingVisual',
                      true,
                    )

Comment on lines 62 to +75
if (__DEV__) {
/**
* The Linter yells because it thinks this conditionally calls an effect,
* but since this is a compile-time flag and not a runtime conditional
* this is safe, and ensures the entire effect is kept out of prod builds
* shaving precious bytes from the output, and avoiding mounting a noop effect
*/
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
if (
innerRef.current &&
!(innerRef.current instanceof HTMLButtonElement) &&
!((innerRef.current as unknown) instanceof HTMLAnchorElement) &&
!((innerRef.current as HTMLElement).tagName === 'SUMMARY')
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
}
}, [innerRef])
// Validate that the element is a semantic button/anchor.
// This runs during render (not in an effect) to avoid a conditional hook call
// that prevents React Compiler from optimizing this component.
const el = innerRef.current
if (
el &&
!(el instanceof HTMLButtonElement) &&
!((el as unknown) instanceof HTMLAnchorElement) &&
!((el as HTMLElement).tagName === 'SUMMARY')
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The new DEV validation runs during render and uses HTMLButtonElement/HTMLAnchorElement. In SSR/non-DOM environments those globals can be undefined, causing a ReferenceError when __DEV__ is true (breaking SSR compatibility). Consider moving this check into an unconditional (isomorphic) effect with the __DEV__ check inside, or guarding with canUseDOM / typeof HTMLButtonElement !== 'undefined' before doing any instanceof checks.

Copilot uses AI. Check for mistakes.
!((el as HTMLElement).tagName === 'SUMMARY')
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The warning message text is grammatically incorrect ("an instanceof a"). Since this is user-facing console output in dev, it should be updated to something clearer like "should be an instance of a semantic button or anchor" (and ideally include the actual rendered tag name).

Suggested change
console.warn('This component should be an instanceof a semantic button or anchor')
console.warn(
`ButtonBase should render a semantic <button> or <a> element, but rendered <${(el as HTMLElement).tagName.toLowerCase()}> instead.`,
)

Copilot uses AI. Check for mistakes.
@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/13891

@primer-integration
Copy link

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

@hectahertz hectahertz added this pull request to the merge queue Feb 18, 2026
Merged via the queue into main with commit 551ec63 Feb 18, 2026
61 checks passed
@hectahertz hectahertz deleted the hectahertz/perf-button-optimize-render branch February 18, 2026 18:18
@primer primer bot mentioned this pull request Feb 18, 2026
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments