Skip to content

Conversation

@romgrk
Copy link
Contributor

@romgrk romgrk commented Dec 7, 2025

Counterpart of #3391

Refactor the useHover hooks into classes.

Results

Screenshot From 2025-12-07 21-38-48 Screenshot From 2025-12-07 21-38-52

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 7, 2025

  • vite-css-base-ui-example

    pnpm add https://pkg.pr.new/mui/base-ui/@base-ui-components/react@3451
    
    pnpm add https://pkg.pr.new/mui/base-ui/@base-ui-components/utils@3451
    

commit: 17f80f5

@mui-bot
Copy link

mui-bot commented Dec 7, 2025

Bundle size report

Bundle Parsed size Gzip size
@base-ui-components/react 🔺+1.16KB(+0.28%) 🔺+214B(+0.16%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@netlify
Copy link

netlify bot commented Dec 7, 2025

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 17f80f5
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/69363a711cae4c0008e8b558
😎 Deploy Preview https://deploy-preview-3451--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@romgrk romgrk added type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. performance internal Behind-the-scenes enhancement. Formerly called “core”. labels Dec 7, 2025
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Dec 9, 2025
@atomiks
Copy link
Contributor

atomiks commented Dec 9, 2025

I don't see a difference with master running the tooltip benchmark here

@romgrk
Copy link
Contributor Author

romgrk commented Dec 10, 2025

This might be missing commits from master or my baseline might not have been up-to-date, but I'll wait before resuming the work here.

Copy link
Member

Choose a reason for hiding this comment

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

Where do performance gains come from?

Copy link
Member

@flaviendelangle flaviendelangle Dec 10, 2025

Choose a reason for hiding this comment

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

useStableCallback runs an effect on every render to make sure the function you call is the one created in the last render (and which has access to up-to-date variables around it).
This goes away with a store class that is initialized once on mount.

Same with React.useCallback, on every render React has to compare the old and new deps.

From what I understand, it's death by a thousand cuts. A lot of very cheap operations on a hook used in lots of components accross the page.
I would not expect similar gains when applying the same logic on a bigger component (but I still think the code is more readable on the Tree View post migration).

Copy link
Member

@flaviendelangle flaviendelangle Dec 10, 2025

Choose a reason for hiding this comment

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

(maybe I'm missing other perf gains in this PR ofc)

For example, I think having lots of useRef in a component can have a small cost that adds up when compared to a class field. But I did not check React implementation of this hook.

Copy link
Member

Choose a reason for hiding this comment

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

@cherniavskii you can have a look at #3444 for some additional context

Copy link
Member

Choose a reason for hiding this comment

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

Is the rule of thumb "extract everything stable into a class" correct?
And for effects or callbacks with dependencies, keep them in the hook (the one that would initialize the class), or in the Hook.use method?

Copy link
Member

Choose a reason for hiding this comment

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

Performancewise, maybe it's the optimal solution
I would keep internal DX in mind if this comes at the cost of our productivity 👀

But I suggest that we let the Base UI team continue the exploration for some time.
Then see where we would have the biggest gain on X (mostly for the new version of the components, I don't suggest to spend time on something that would not benefit the new versions) and start from there.

I don't think it's worth converting a hook / component that has 2 React.useCallback and handles a big amount of data to a class, because the gain will be marginal (the React.useCallback is not the bulk of the work).
But I honestly don't have a clear vision of where this pattern should be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From what I understand, it's death by a thousand cuts.

This is it.

For example, I think having lots of useRef in a component can have a small cost that adds up when compared to a class field. But I did not check React implementation of this hook.

For sure, instead of having on single big object in a contiguous memory region, you have many small objects spread in memory, so the cache locality is bad. React compounds the issue by using a linked list instead of an arrya to store hook state.

const hookObject = {
  fields: {
    value: 1,
    next: {
      value: 2,
      next: {
        value: 3,
        next: null, } } }
}

const classObject = {
  fieldA: 1,
  fieldB: 2,
  fieldC: 3,
}

But I suggest that we let the Base UI team continue the exploration for some time.

Yes, there are ergonomy problems with mixing hooks with classes due to typescript limitations, and we still need to think about sync'ing props at the right time in the render pipeline to avoid hard to debug issues. I want to test this out slowly with a few hooks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Behind-the-scenes enhancement. Formerly called “core”. performance PR: out-of-date The pull request has merge conflicts and can't be merged. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants