Skip to content

perf(ThemeProvider): Reduce unnecessary renders and effect cascades#7695

Merged
liuliu-dev merged 7 commits intomainfrom
improve-theme-provider-perf
Mar 30, 2026
Merged

perf(ThemeProvider): Reduce unnecessary renders and effect cascades#7695
liuliu-dev merged 7 commits intomainfrom
improve-theme-provider-perf

Conversation

@mattcosta7
Copy link
Copy Markdown
Contributor

@mattcosta7 mattcosta7 commented Mar 24, 2026

Summary

Performance improvements to ThemeProvider in both @primer/react and @primer/styled-react by replacing useState + useEffect patterns with useSyncExternalStore. All changes maintain identical user-facing behavior.

Changes

1. useSyncExternalStore for SSR hydration handoff

Before: setTimeoutReactDOM.flushSync(setColorMode(resolved))setColorMode(original) — 3 renders, forced sync flush, temporarily corrupted colorMode state. The previous commit simplified this to useState + useEffect, but still caused a post-hydration re-render.

After: useSyncExternalStore with getServerSnapshot reading the <script type="application/json"> tag during hydration, and getSnapshot resolving from client state after. Zero post-hydration re-renders. No effects.

Also removes the ReactDOM import entirely.

2. useSyncExternalStore for useSystemColorMode

Before: useState(getSystemColorMode) + useEffect subscribing to matchMedia changes — potential stale-then-update flicker between init and effect, and a redundant setSystemColorMode call to catch changes during the gap.

After: useSyncExternalStore(subscribeToSystemColorMode, getSystemColorMode, () => "day") — always current, no effect gap, no state.

3. Cached getServerHandoff

DOM read + JSON.parse is now cached per ID in a Map. The <script> tag content is static (written once during SSR), so subsequent calls return the cached result.

4. Memoized context value

The ThemeContext.Provider value was a new object literal every render, causing all consumers to re-render even when nothing changed. Now wrapped in useMemo.

Scenario analysis

Scenario Before (original) After
No SSR handoff (common) Effect skips No effect exists
SSR match 1+ extra renders via effect 0 extra renders (useSyncExternalStore handles transition)
SSR mismatch 3 renders via flushSync cascade Synchronous correction, no cascade
System color mode changes useEffect subscribes, potential flicker useSyncExternalStore — always synchronous
matchMedia calls 1 per render (new MediaQueryList allocation) 1 per render (browser returns same cached object per spec)
Repeated parent renders DOM read + JSON.parse every time Cached after first call

Testing

All existing ThemeProvider tests pass (20 in @primer/react, 29 in @primer/styled-react).

@mattcosta7 mattcosta7 requested a review from a team as a code owner March 24, 2026 01:51
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 24, 2026

🦋 Changeset detected

Latest commit: e11672a

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

This PR includes changesets to release 2 packages
Name Type
@primer/react Patch
@primer/styled-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

@mattcosta7 mattcosta7 self-assigned this Mar 24, 2026
@mattcosta7 mattcosta7 marked this pull request as draft March 24, 2026 01:51
@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.

@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 24, 2026
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 refactors ThemeProvider implementations (both @primer/react and @primer/styled-react) to simplify SSR color-mode handoff handling by removing the ReactDOM-based flushSync workaround and by making context value construction more stable.

Changes:

  • Replace the server color-mode passthrough useRef + ReactDOM.flushSync workaround with a serverColorMode state that is cleared after hydration.
  • Memoize the ThemeContext.Provider value via useMemo to reduce unnecessary context value churn.
  • Remove the unused react-dom import from both ThemeProvider implementations.

Reviewed changes

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

File Description
packages/styled-react/src/components/ThemeProvider.tsx Aligns styled-react ThemeProvider SSR handoff logic with a state-based server passthrough and memoized context value.
packages/react/src/ThemeProvider.tsx Updates core ThemeProvider SSR handoff logic to remove ReactDOM.flushSync usage and memoizes the context value.
Comments suppressed due to low confidence (1)

packages/react/src/ThemeProvider.tsx:88

  • ThemeProvider has an existing unit test suite, but the preventSSRMismatch / server handoff path introduced here isn’t covered. Please add a test that simulates server handoff data (e.g., by mocking useId to a stable value and inserting a matching __PRIMER_DATA_<id>__ script element) and asserts that the provider initially uses the server-resolved color mode and then switches to client resolution after hydration.
  // Lazy initializer reads DOM + parses JSON once instead of every render
  const [serverColorMode, setServerColorMode] = React.useState<ColorMode | undefined>(
    () => getServerHandoff(uniqueDataId).resolvedServerColorMode,
  )

  const [colorMode, setColorMode] = useSyncedState(props.colorMode ?? fallbackColorMode ?? defaultColorMode)
  const [dayScheme, setDayScheme] = useSyncedState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme)
  const [nightScheme, setNightScheme] = useSyncedState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme)
  const systemColorMode = useSystemColorMode()
  const resolvedColorMode = serverColorMode ?? resolveColorMode(colorMode, systemColorMode)
  const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme)
  const {resolvedTheme, resolvedColorScheme} = React.useMemo(
    () => applyColorScheme(theme, colorScheme),
    [theme, colorScheme],
  )

  // After hydration, clear the server passthrough so client-side color mode takes over
  React.useEffect(
    function clearServerPassthrough() {
      if (serverColorMode !== undefined) {
        setServerColorMode(undefined)
      }
    },
    [serverColorMode],
  )

@mattcosta7 mattcosta7 changed the title improve theme provider perf(ThemeProvider): Reduce unnecessary renders and effect cascades Mar 24, 2026
@github-actions github-actions bot temporarily deployed to storybook-preview-7695 March 24, 2026 02:02 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-7695 March 24, 2026 02:12 Inactive
@mattcosta7 mattcosta7 marked this pull request as ready for review March 24, 2026 02:17
…m color mode

- Replace useState + useEffect SSR hydration handoff with useSyncExternalStore
- Replace useState + useEffect in useSystemColorMode with useSyncExternalStore
- Cache getServerHandoff DOM read + JSON.parse per ID
- Remove post-hydration re-render and effect cascades
@github-actions github-actions bot requested a deployment to storybook-preview-7695 March 25, 2026 19:16 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7695 March 25, 2026 19:27 Inactive
@mattcosta7
Copy link
Copy Markdown
Contributor Author

CI in gh/gh-ui wil fail until the current release merges and removes the unused feature flag

@jonrohan jonrohan added the integration-tests: skipped manually Changes in this PR do not require an integration test label Mar 25, 2026
@primer-integration
Copy link
Copy Markdown

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

@primer-integration
Copy link
Copy Markdown

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

@mattcosta7 mattcosta7 added this pull request to the merge queue Mar 30, 2026
@liuliu-dev liuliu-dev removed this pull request from the merge queue due to a manual request Mar 30, 2026
@liuliu-dev liuliu-dev added this pull request to the merge queue Mar 30, 2026
Merged via the queue into main with commit 780fc3d Mar 30, 2026
53 checks passed
@liuliu-dev liuliu-dev deleted the improve-theme-provider-perf branch March 30, 2026 16:48
@primer primer bot mentioned this pull request Mar 30, 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 integration-tests: skipped manually Changes in this PR do not require an integration test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants