Skip to content

Commit a293af9

Browse files
authored
Improve Menu component performance (#3685)
This PR improves the performance of the `Menu` component. Before this PR, the `Menu` component is built in a way where all the state lives in the `Menu` itself. If state changes, everything re-renders and re-computes the necessary derived state. However, if you have a 1000 items, then every time the active item changes, all 1000 items have to re-render. To solve this, we can move the state outside of the `Menu` component, and "subscribe" to state changes using the `useSlice` hook introduced in #3684. This will allow us to subscribe to a slice of the state, and only re-render if the computed slice actually changes. If the active item changes, only 3 things will happen: 1. The `MenuItems` will re-render and have an updated `aria-activedescendant` 2. The `MenuItem` that _was_ active, will re-render and the `data-focus` attribute wil be removed. 3. The `MenuItem` that is now active, will re-render and the `data-focus` attribute wil be added. Another improvement is that in order to make sure that your arrow keys go to the correct item, we need to sort the DOM nodes and make sure that we go to the correct item when using arrow up and down. This sorting was happening every time a new `MenuItem` was registered. Luckily, once an array is sorted, you don't have to do a lot, but you still have to loop over `n` items which is not ideal. This PR will now delay the sorting until all `MenuItem`s are registered. On that note, we also batch the `RegisterItem` so we can perform a single update instead of `n` updates. We use a microTask for the batching (so if you only are registering a single item, you don't have to wait compared to a `setTimeout` or a `requestAnimationFrame`). ## Test plan 1. All tests still pass 2. Tested this in the browser with a 1000 items. In the videos below the only thing I'm doing is holding down the `ArrowDown` key. Before: https://github.com/user-attachments/assets/513b02c1-fc69-47f3-a97e-c56d44dd585a After: https://github.com/user-attachments/assets/266236a0-b64a-4322-9a54-ead7fb62191f
1 parent e2a6376 commit a293af9

File tree

4 files changed

+515
-401
lines changed

4 files changed

+515
-401
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Improve `Menu` component performance ([#3685](https://github.com/tailwindlabs/headlessui/pull/3685))
1113

1214
## [2.2.1] - 2025-04-04
1315

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createContext, useContext, useMemo } from 'react'
2+
import { MenuMachine } from './menu-machine'
3+
4+
export const MenuContext = createContext<MenuMachine | null>(null)
5+
export function useMenuMachineContext(component: string) {
6+
let context = useContext(MenuContext)
7+
if (context === null) {
8+
let err = new Error(`<${component} /> is missing a parent <Menu /> component.`)
9+
if (Error.captureStackTrace) Error.captureStackTrace(err, useMenuMachine)
10+
throw err
11+
}
12+
return context
13+
}
14+
15+
export function useMenuMachine({ __demoMode = false } = {}) {
16+
return useMemo(() => MenuMachine.new({ __demoMode }), [])
17+
}

0 commit comments

Comments
 (0)