# How to Optimize Zustand Performance for Frequently Changing State: 6 Proven Strategies

> Optimize Zustand performance with 6 strategies for frequently changing state. Learn to use selectors, shallow equality, and batching to minimize re-renders and boost app speed.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: performance
- Published: 2026-03-06

---

**Use selector functions combined with shallow equality checks to subscribe components only to specific state slices, and leverage functional `setState` batching alongside middleware like `subscribeWithSelector` to minimize re-renders and computations during high-frequency updates.**

When building React applications with the **pmndrs/zustand** library, state that changes on every animation frame or rapid user interaction can quickly become a performance bottleneck. Implementing patterns derived directly from the core source code ensures your UI remains responsive even under sustained high-frequency updates.

## Select Specific State Slices with `useStore`

The **`useStore`** hook in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) (lines 26-38) bridges the Zustand store to React components using `React.useSyncExternalStore`. Without a selector, this subscribes the component to the entire store, causing re-renders whenever any property changes. By supplying a selector function, you restrict the subscription to only the returned slice, ensuring the component re-renders solely when that specific data changes.

```tsx
import create from 'zustand'
import { useShallow } from 'zustand/shallow'

// Store holds a large object, but we only need the counter value
const useStore = create((set) => ({
  counter: 0,
  largeData: { /* … many fields … */ },
  inc: () => set((s) => ({ counter: s.counter + 1 })),
}))

function Counter() {
  // Selector returns an object; useShallow prevents re‑render if only other fields change
  const { counter, inc } = useStore(
    (state) => ({ counter: state.counter, inc: state.inc }),
    useShallow
  )
  return (
    <>
      <div>{counter}</div>
      <button onClick={inc}>+</button>
    </>
  )
}

```

## Stabilize Selectors with Shallow Equality

When selectors return objects or arrays created inline, `Object.is` comparisons fail on every render because the reference changes. The **`shallow`** utility in [`src/vanilla/shallow.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla/shallow.ts) (lines 48-74) iterates through iterables, maps, and plain objects to verify property-wise equality using `Object.is`. Wrapping your selector with **`useShallow`** (re-exported in [`src/react/shallow.ts`](https://github.com/pmndrs/zustand/blob/main/src/react/shallow.ts)) prevents spurious re-renders when values remain identical despite new object references.

## Isolate Side Effects with `subscribeWithSelector` Middleware

For logic that must react to state changes without triggering React renders—such as debouncing, analytics, or localStorage syncing—the **`subscribeWithSelector`** middleware in [`src/middleware/subscribeWithSelector.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/subscribeWithSelector.ts) (lines 46-71) exposes a selector-based subscription API. This creates listeners that receive only the derived slice and support custom equality functions, isolating side effects from the UI rendering cycle.

```ts
import create from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

const useStore = create(
  subscribeWithSelector((set, get) => ({
    items: [] as number[],
    add: (n: number) => set((s) => ({ items: [...s.items, n] })),
  }))
)

// Log the length of the array only when it changes
useStore.subscribe(
  (state) => state.items.length,
  (len) => console.log('Item count:', len)
)

```

## Batch Rapid Updates with Functional `setState`

The **`setState`** implementation in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) (lines 66-80) efficiently batches multiple updates. When you pass a function to `setState`, it receives the current state and returns the next state; the internal `Object.is` guard ensures listeners only fire when the resolved state actually differs. This allows rapid successive calls—such as drag coordinates updating on `mousemove`—to coalesce into a single notification cycle.

```ts
import create from 'zustand'

const useStore = create((set) => ({
  x: 0,
  y: 0,
  move: (dx: number, dy: number) =>
    set((s) => ({ x: s.x + dx, y: s.y + dy })), // single listener call
}))

// Called many times per animation frame
function onMouseMove(e: MouseEvent) {
  useStore.getState().move(e.movementX, e.movementY)
}
window.addEventListener('mousemove', onMouseMove)

```

## Preserve Reference Equality with Immer Middleware

Large object spreads in updates force shallow comparison checks to traverse every property. The **Immer** middleware in [`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts) produces new objects only for mutated branches while preserving reference equality for unchanged data. This structural sharing minimizes the work required by `Object.is` and shallow equality checks during high-frequency update patterns.

## Keep the Store Pure and Computation-Free

Stores should contain only data, not expensive transformations. Computing derived data inside selectors or memoized hooks (`useMemo`) rather than inside the store prevents unnecessary recalculation on every state access, keeping the core state object lightweight.

## Summary

- Use **selector functions** with `useStore` ([`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts)) to limit component subscriptions to specific state slices
- Apply **`useShallow`** from [`src/vanilla/shallow.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla/shallow.ts) to prevent re-renders caused by object reference changes
- Implement **`subscribeWithSelector`** middleware ([`src/middleware/subscribeWithSelector.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/subscribeWithSelector.ts)) for side effects without triggering UI updates
- Batch rapid changes using **functional `setState`** in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) to reduce listener notification frequency
- Leverage **Immer middleware** ([`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts)) for structural sharing and cheap equality checks
- Maintain **pure stores** by moving derived data calculations outside the state object into selectors

## Frequently Asked Questions

### How does Zustand prevent unnecessary re-renders by default?

Zustand's core `setState` logic in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) uses `Object.is` to compare the next state against the previous state immediately after resolution. If the values are identical, the update aborts and the `listeners` collection receives no notifications, naturally preventing React component re-renders when data has not actually changed.

### When should I use `useShallow` versus a custom equality function?

Use the built-in **`useShallow`** hook (from [`src/react/shallow.ts`](https://github.com/pmndrs/zustand/blob/main/src/react/shallow.ts)) when your selector returns arrays or objects containing primitive values that require shallow comparison. For complex deep equality checks or domain-specific comparison logic, provide a custom equality function to `subscribeWithSelector` or as the second argument to `useStore`.

### Can I use Immer and shallow equality together?

Yes. **Immer** from [`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts) maintains reference equality for unchanged branches of the state tree, which pairs perfectly with `useShallow` or manual `shallow` checks. This combination ensures object references remain stable across renders unless the underlying data actually mutates, maximizing the effectiveness of shallow comparisons.

### Does functional `setState` batch updates automatically?

Yes. When calling `setState` with a function multiple times within the same execution tick, Zustand processes each updater sequentially against the current state but only notifies listeners once after all updaters resolve, provided the final state differs from the original. This batching behavior is inherent to the implementation in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) (lines 66-80).