-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Prepare for performance improvements #3684
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
Conversation
This helps with a development build of React for example in the browser instead of minified React.
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
// Assumption: everything else is considered equal | ||
// TODO: Add more specific checks for other types when needed such as Map, | ||
// Set, ... | ||
return true |
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 feel like defaulting to true
here could lead to some subtle bugs. I think defaulting to false
unless you can prove a
and b
are equal is a safer choice.
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.
Aside: you could probably check for [Symbol.iterator]
and iterate both as the base for an equality check on Sets, Maps, etc…
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.
Yeah fair, and for objects I could just use Object.entries(obj)
🤔
Also handles Map, Set, ...
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
This PR improves the performance of the `Listbox` component. Before this PR, the `Listbox` component is built in a way where all the state lives in the `Listbox` itself. If state changes, everything re-renders and re-computes the necessary derived state. However, if you have a 1000 options, then every time the active option changes, all 1000 options have to re-render. To solve this, we can move the state outside of the `Listbox` 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 option changes, only 3 things will happen: 1. The `ListboxOptions` will re-render and have an updated `aria-activedescendant` 2. The `ListboxOption` that _was_ active, will re-render and the `data-focus` attribute wil be removed. 3. The `ListboxOption` 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 option, we need to sort the DOM nodes and make sure that we go to the correct option when using arrow up and down. This sorting was happening every time a new `ListboxOption` was registered. Luckily, once an array is sorted, you don't have to do a lot, but you still have to loop over `n` options which is not ideal. This PR will now delay the sorting until all `ListboxOption`s are registered. On that note, we also batch the `RegisterOption` 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 option, 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 2000 options. In the videos below the only thing I'm doing is holding down the `ArrowDown` key. Before: https://github.com/user-attachments/assets/a2850c84-57f6-428a-aa51-e6f83d2aee97 After: https://github.com/user-attachments/assets/157c6e99-5da8-4d72-87c6-a5e34f122531
This PR improves the performance of the `Combobox` component. This is a similar implementation as: - #3685 - #3688 Before this PR, the `Combobox` component is built in a way where all the state lives in the `Combobox` 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 `Combobox` 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 `ComboboxOptions` will re-render and have an updated `aria-activedescendant` 2. The `ComboboxOption` that _was_ active, will re-render and the `data-focus` attribute wil be removed. 3. The `ComboboxOption` that is now active, will re-render and the `data-focus` attribute wil be added. The `Combobox` component already has a `virtual` option if you want to render many many more items. This is a bit of a different model where all the options are passed in via an array instead of rendering all `ComboboxOption` components immediately. Because of this, I didn't want to batch the registration of the options as part of this PR (similar to what we do in the `Menu` and `Listbox`) because it behaves differently compared to what mode you are using (virtual or not). Since not all components will be rendered, batching the registration until everything is registered doesn't really make sense in the general case. However, it does make sense in non-virtual mode. But because of this difference, I didn't want to implement this as part of this PR and increase the complexity of the PR even more. Instead I will follow up with more PRs with more improvements. But the key improvement of looking at the slice of the data is what makes the biggest impact. This also means that we can do another release once this is merged. Last but not least, recently we fixed a bug where the `Combobox` in `virtual` mode could crash if you search for an item that doesn't exist. To solve it, we implemented a workaround in: - #3678 Which used a double `requestAnimationFrame` to delay the scrolling to the item. While this solved this issue, this also caused visual flicker when holding down your arrow keys. I also fixed it in this PR by introducing `patch-package` and work around the issue in the `@tanstack/virtual-core` package itself. More info: 96f4da7 Before: https://github.com/user-attachments/assets/132520d3-b4d6-42f9-9152-57427de20639 After: https://github.com/user-attachments/assets/41f198fe-9326-42d1-a09f-077b60a9f65d ## 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/945692a3-96e6-4ac7-bee0-36a1fd89172b After: https://github.com/user-attachments/assets/98a151d0-16cc-4823-811c-fcee0019937a
This PR improves the performance of the `Combobox` component. This is a similar implementation as: - #3685 - #3688 Before this PR, the `Combobox` component is built in a way where all the state lives in the `Combobox` 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 `Combobox` 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 `ComboboxOptions` will re-render and have an updated `aria-activedescendant` 2. The `ComboboxOption` that _was_ active, will re-render and the `data-focus` attribute wil be removed. 3. The `ComboboxOption` that is now active, will re-render and the `data-focus` attribute wil be added. The `Combobox` component already has a `virtual` option if you want to render many many more items. This is a bit of a different model where all the options are passed in via an array instead of rendering all `ComboboxOption` components immediately. Because of this, I didn't want to batch the registration of the options as part of this PR (similar to what we do in the `Menu` and `Listbox`) because it behaves differently compared to what mode you are using (virtual or not). Since not all components will be rendered, batching the registration until everything is registered doesn't really make sense in the general case. However, it does make sense in non-virtual mode. But because of this difference, I didn't want to implement this as part of this PR and increase the complexity of the PR even more. Instead I will follow up with more PRs with more improvements. But the key improvement of looking at the slice of the data is what makes the biggest impact. This also means that we can do another release once this is merged. Last but not least, recently we fixed a bug where the `Combobox` in `virtual` mode could crash if you search for an item that doesn't exist. To solve it, we implemented a workaround in: - #3678 Which used a double `requestAnimationFrame` to delay the scrolling to the item. While this solved this issue, this also caused visual flicker when holding down your arrow keys. I also fixed it in this PR by introducing `patch-package` and work around the issue in the `@tanstack/virtual-core` package itself. More info: 96f4da7 Before: https://github.com/user-attachments/assets/132520d3-b4d6-42f9-9152-57427de20639 After: https://github.com/user-attachments/assets/41f198fe-9326-42d1-a09f-077b60a9f65d ## 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/945692a3-96e6-4ac7-bee0-36a1fd89172b After: https://github.com/user-attachments/assets/98a151d0-16cc-4823-811c-fcee0019937a
This PR is just a chore to prepare for future performance optimizations. Essentially I want to improve the performance of the
Menu
,Listbox
andCombobox
components but I want to do it in separate PRs such that reverting the improvements can be done if needed.This PR just sets up a
Machine
for state machines, and adds some helpers such as auseSlice
to calculate parts of the state machine. Component using theuseSlice
will only re-render if the slice changes.So apart from adding a library (
useSyncExternalStoreWithSelector
) and adding some setup code. Nothing in this PR changes the behavior of the components.