-
-
Notifications
You must be signed in to change notification settings - Fork 285
[internal] Performance: fast useStore implementation
#3445
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
base: master
Are you sure you want to change the base?
Conversation
commit: |
Bundle size report
Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
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 introduces a performance optimization for the useStore hook by implementing a "fast hooks" pattern that reduces store subscriptions from multiple-per-component to one-per-component. The optimization wraps components to establish an instance context, enabling useSyncExternalStore to be called once while accumulating multiple selectors. This is applied to React 19+ and results in 15-35% faster mount times for tooltip triggers.
Key changes:
- New
fastHooksmodule with component wrappers (create,createRef) that manage per-instance hook state useStoreFastimplementation that batches multipleuseStorecalls into a single subscription- Updated
TooltipRootandTooltipTriggerto use the new fast hooks wrappers
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
packages/utils/src/fastHooks.ts |
New module providing component wrappers that establish instance context for optimized hook batching |
packages/utils/src/store/useStore.ts |
Adds useStoreFast implementation that leverages instance context to batch store subscriptions |
packages/react/src/tooltip/trigger/TooltipTrigger.tsx |
Wraps component with fastHooks.createRef to enable store subscription optimization |
packages/react/src/tooltip/root/TooltipRoot.tsx |
Wraps component with fastHooks.create to enable store subscription optimization |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/utils/src/fastHooks.ts
Outdated
| index: number; | ||
| }; | ||
|
|
||
| let currentInstance: Instance | undefined = undefined; |
Copilot
AI
Dec 6, 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.
The currentInstance global variable could cause issues in concurrent rendering scenarios (e.g., React 18+ concurrent features, Server Components). If multiple components render concurrently, they could overwrite each other's currentInstance, leading to hooks being associated with the wrong component instance. Consider using a context-based approach or ensure this is only used in synchronous rendering contexts.
| after: (instance: any) => void; | ||
| }; | ||
|
|
||
| const hooks: HookType[] = []; |
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.
Singleton in the utils package here, but I guess it's not harmful, even if utils package gets duplicated, right?
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.
I've been considering de-duplicating the global state at runtime (const hooks = globalThis.fastHooks_v1 ??= []), but it's indeed not harmful. There is no logical bug from doing so, only lesser gains.
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.
Did you consider a more explicit approach, where a single store.useState accepts multiple selectors and composes them together?
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.
Not really possible, some store calls might be spread across multiple files & hooks. The "popup" logic is shared by many components (tooltip, menu, popover) with hooks like useFocus or useTriggerRegistration accessing the common state interface from outside the component.
Problem
The
useStore()hook can be called many times per component. The naive implementation defers to React'suseSyncExternalStore(), which creates one store subcription per call, which means one component ends up with multiple subscriptions to the store. This is unefficient because the component re-renders as an unit, and only requires 1 subscription.Solution
This PR introduces a wrapper around components to establish an
instancecontext object per component instance, which we can use to calluseSyncExternalStore()at most once per component. Subsequent calls just accumulate the selectors in an array, without callinguseSyncExternalStore()again.The one downside of this solution is that we need to wrap components to make store accesses faster, e.g.:
Results
I observe a decrease of 15-35% in mount time for the contained tooltip triggers benchmark.