Zustand Performance Study

Avoiding Destructuring in the Selector

const name = useUserState(state => state.name)

vs

const { name } = useUserState(state => state); or const { name } = useUserState();

The second option affects performance by causing unnecessary re-renders of the component. This happens because the selector subscribes to the component, leading to re-rendering whenever the selector’s return value changes.

In the first code, it examines on the variable ‘name,’ which is a String type. However, in the second scenario, it deals with an object. Even though the content within the object, { name }, remains the same, the selector consistently gets a different reference address for the object { name } with each time the store state is updated. Consequently, the selector misinterprets changes to the component, causing unnecessary re-renders.

UseShallow for Enhanced Performance

Within Zustand, a beneficial native hook named useShallow proves invaluable for optimizing performance:

const { name } = useUserState(useShallow(state => state))

Essentially, useShallow conducts a deep comparison of objects instead of solely checking reference addresses. This allows for the use of destructuring without making a performance hit.

Using this feature makes it easier to write more advanced and straightforward code, as shown below:

Selecting multiple values with single line of code

const { name, age } = useUserState(useShallow(state => return{ state.name, state.age } ));
const [ name, age ] = useUserState(useShallow(state => [ state.name, state.age ] ));

By using useShallow, you can elegantly select multiple values in a single line, enhancing both readability and performance.

Select Functions From Zustand Store

Many developers often retrieve functions from the Zustand store using the following code:

const setName = useUserState(state => state.setName)

or

const { setName } = useUserState()

Unfortunately, both of these codes don’t prevent components from re-rendering every time the store state is updated. This happens because, in JavaScript, a function is treated as an object, and the reference address of an object changes each time the store state is updated.

One way to work around this is by using useShallow:

const { setName } = useUserState(useShallow(state => state.setName))

This prevents re-rendering but still checks for changes every time the store state is updated, making it less than optimal. A better approach is:

const { setName } = useUserState.getState()

The getState() method returns the current state of the Zustand store without creating a selector. As a result, it avoids adding a subscription to the component and eliminates the need for the component to check for changes on every time the store state is updated.

While this method works well for performance, there might be an even better way. Consider exporting the functions in the store file and then directly importing them into the component files, like this:

import { setName } from 'user-state.js'

I am currently researching how to implement this approach, and unfortunately, I don’t have a code sample to demonstrate how it looks in the user-state.js file yet.

Testing

Testing is a reliable way to make sure your frontend application runs smoothly. It helps ensure that a component isn’t re-rendered when it shouldn’t be, which is crucial for maintaining good performance. These subtle performance issues are hard to spot just by looking at the application with human eyes, but tests provide a dependable way to catch these changes.