Skip to content

Performance issues when many <Menu> elements exist on one page #3630

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

Closed
apazzolini opened this issue Feb 4, 2025 · 2 comments · Fixed by #3722
Closed

Performance issues when many <Menu> elements exist on one page #3630

apazzolini opened this issue Feb 4, 2025 · 2 comments · Fixed by #3722

Comments

@apazzolini
Copy link

What package within Headless UI are you using?

@headlessui/react

What version of that package are you using?

v2.2.0

What browser are you using?

Firefox (but happens on any browser)

Reproduction URL

Describe your issue

As of @headlessui/react v2.1.0, opening / closing a Menu causes all other Menus on the same page to re-render, resulting in noticeable lag. To reproduce, visit the above sandbox and attempt to open a menu on any row.

Thank you!

@liri2006
Copy link

liri2006 commented Apr 4, 2025

Hi, any change to merge the fix by @rkoval?

@RobinMalfait

RobinMalfait added a commit that referenced this issue May 10, 2025
This PR fixes a performance issue where all components using the
`useIsTopLayer` hook will re-render when the hook changes.

For context, the internal hook is used to know which component is the
top most component. This is important in a situation like this:

```
<Dialog>
  <Menu />
</Dialog>
```

If the Menu inside the Dialog is open, it is considered the top most
component. Clicking outside of the Menu or pressing escape should only
close the Menu and not the Dialog.

This behavior is similar to the native `#top-layer` you see when using
native dialogs for example.

The issue however is that the `useIsTopLayer` subscribes to an external
store which is shared across all components. This means that when the
store changes, all components using the hook will re-render.

To make things worse, since we can't use these hooks unconditionally,
they will all be subscribed to the store even if the Menu component(s)
are not open.

To solve this, we will use a new state machine and use the `useMachine`
hook. This internally uses a `useSyncExternalStoreWithSelector` to
subscribe to the store.

This means that the component will only re-render if the state computed
by the selector changes.

This now means that at most 2 components will re-render when the store
changes:

1. The component that _was_ in the top most position
2. The component that is going to be in the top most position

Fixes: #3630
Closes: #3662


# Test plan

Behavior before: notice how all Menu components re-render:


https://github.com/user-attachments/assets/3172b632-0fa4-42db-970c-39efc827dd84

After this change, only the Menu that was opened / closed will
re-render:


https://github.com/user-attachments/assets/5d254bfc-5233-47a7-94d3-eb7a8593e14f
@RobinMalfait
Copy link
Member

Hey!

Went with a different approach to solve this issue. This should be fixed by #3722, and will be available in the next release.

You can already try it using:

  • npm install @headlessui/react@insiders.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants