zustand 5.0.15 vs @reduxjs/toolkit 2.13.0 + react-redux 9.3.0 on React 19.3.0 (jsdom, NODE_ENV=production). 1,000 selector-subscribed rows, 300 flushSync updates x 7 trials, 2 runs: per-update median 0.137/0.139 ms vs 0.676/0.694 ms (~5x); 1 row re-rendered per update in both. Store-only: 1,376,191 vs 6,502 updates/s; Immer autoFreeze off lifts RTK to 386,966. Gzip (React external): 387 B vs 9,708 B. Fixture /workspace/bench-zustand-rtk/.
5 min read
I bumped one row in a 1,000-row React 19 list 300 times per trial: Zustand 5.0.15 took a median 0.14 ms per update, Redux Toolkit 2.13.0 with react-redux 9.3.0 took 0.68 ms, about 5× slower. Both re-rendered exactly 1 row per update, and Zustand's hook shipped 0.4 KB gzip against 9.7 KB for RTK plus react-redux.
Short answer: pick Zustand for new React apps whose state is mostly client UI state. Keep Redux Toolkit if you already run it, rely on Redux DevTools time travel, or use RTK Query. No affiliate links in this post.
{ id, done, count }. The action bumps one item's count. Zustand uses an immutable spread; RTK uses a createSlice reducer with Immer.<li> rows, each subscribed to its own item with a selector (useStore vs useSelector). 300 flushSync updates per trial, 7 trials after a warm-up, NODE_ENV=production, run twice.| Setup (1,000 rows, 300 updates) | Median ms per update | Rows re-rendered per update |
|---|---|---|
Zustand useStore + selector | 0.137 | 1 |
RTK default middleware + useSelector | 0.676 | 1 |
| RTK with immutable/serializable checks off | 0.616 | 1 |
| RTK with checks off and Immer autoFreeze off | 0.758 | 1 |
Both libraries got the important part right: one changed item re-rendered one row. The gap is overhead around that render. Turning off RTK's dev checks or Immer freezing did not close it in the React test. At 0.68 ms per update, RTK is still far inside a 16 ms frame. You would only feel this with many updates per second, such as live tickers, drag interactions or a large table. For DOM test environments, my jsdom vs happy-dom run has the timing side.
| Store only (Node, production) | Updates per second |
|---|---|
Zustand setState with spread | 1,376,191 |
| RTK default middleware | 6,502 |
| RTK with checks off | 6,657 |
| RTK with checks off and Immer autoFreeze off | 386,966 |
Without React, Zustand ran about 200× more updates per second. Turning off RTK's immutable and serializable checks barely moved it. Turning off Immer's auto-freeze took RTK from 6,657 to 386,966 updates/s. In development mode the default RTK store dropped to 2,720 updates/s, because those dev checks walk the state too. Treat this as the cost of Immer on a 1,000-item map, not something you will see one-for-one in a UI. The React numbers above are the ones to plan with.
Yes, a little. With React excluded and production mode set, esbuild minified Zustand's create to 624 B (387 B gzip). configureStore + createSlice + react-redux's Provider, useSelector and useDispatch came to 25,039 B (9,708 B gzip). Installed trees were about 95 KB for Zustand against about 9.2 MB for RTK, react-redux, redux, immer, reselect and redux-thunk. To keep that number honest in CI, see size-limit vs bundlesize.
| Situation | My pick |
|---|---|
| New React app, mostly UI state | Zustand |
| Existing RTK app with passing tests | Stay on RTK |
| Heavy server data fetching with caching | RTK Query, or a data-fetching library beside Zustand |
| Large team that wants one enforced pattern | RTK |
| High-frequency updates (drag, tickers, big tables) | Zustand |
| Need DevTools time travel out of the box | RTK |
If you migrate, remove the leftovers afterwards: knip vs depcheck finds unused packages. Store tests run fine in either runner from my Jest vs Vitest comparison. For React 19 features that touch rendering, see my React 19.3 notes.
Bottom line: in a 1,000-row React 19 list, Zustand 5.0.15 handled an update in 0.14 ms against 0.68 ms for Redux Toolkit 2.13.0, with the same 1 row re-rendered, and shipped 0.4 KB gzip against 9.7 KB. Who should not switch: teams with a working RTK codebase, RTK Query endpoints or a habit of debugging through Redux DevTools. 0.5 ms per update will not justify a rewrite.
How this was made: I installed both libraries on the ShopperCove box, rendered the same 1,000-row list under jsdom, timed flushSync updates and store-only dispatch loops, measured esbuild bundle sizes and recorded the JSON. The write-up was drafted with AI help and checked against that output.
Lab evidence
Hands-on on ShopperCove box 6 Oct 2026 ~00:38-00:42 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). zustand 5.0.15 vs @reduxjs/toolkit 2.13.0 (redux 5.0.1, immer 11.1.21) + react-redux 9.3.0, React 19.3.0, jsdom 29.1.1. Fixture /workspace/bench-zustand-rtk/: normalized 1,000-item map; action bumps one item count (Zustand spread vs createSlice/Immer). React: 1,000 li rows each with own selector, 300 flushSync updates/trial, 7 trials after warm-up, NODE_ENV=production, 2 runs. Per-update medians run2: Zustand 0.137 ms, RTK default 0.676, RTK checks off 0.616, RTK checks off + autoFreeze off 0.758 (run1 0.139/0.694/0.677/0.801). Renders per update 1 for all. Mount 10.6 vs 9.0 ms. Store-only (20k updates x 9 rounds, production): Zustand 1,376,191/s; RTK 6,502; checks off 6,657; autoFreeze off 386,966; dev-mode RTK default 2,720. esbuild minify/gzip9, React external: zustand create 624/387 B; RTK+react-redux 25,039/9,708 B. Disk ~95 KB vs ~9.2 MB. Raw: res-react*.json, res-store-*.json, res-bundle.json. Not tested: DevTools, RTK Query, entity adapter, Zustand middleware, React Native, real browsers, SSR. No affiliate.