ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 60

  1. Blog

Zustand vs Redux Toolkit: 0.14 ms vs 0.68 ms per Update

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/.

Aditya Challa·5 October 2026·5 min read

Hands-on
On this page
  1. What I tested
  2. How big was the React update gap?
  3. Why was the store-only gap so large?
  4. Does bundle size matter here?
  5. Should you switch from Redux Toolkit to Zustand?
  6. Sources
  7. Related

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.

What I tested

  • Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 00:38–00:42 IST.
  • Versions: zustand 5.0.15, @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, Node 22.20.0.
  • Fixture: /workspace/bench-zustand-rtk/. One normalized map of 1,000 items { id, done, count }. The action bumps one item's count. Zustand uses an immutable spread; RTK uses a createSlice reducer with Immer.
  • React test: 1,000 <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.
  • Store test: no React, 1 subscriber, 20,000 updates per round, 9 timed rounds after one warm round.
  • Findings: React per-update median 0.137 ms (Zustand) vs 0.676 ms (RTK) on run 2, and 0.139 vs 0.694 ms on run 1. Mounting 1,000 rows took about 10.6 ms vs 9.0 ms, so RTK mounted slightly faster. Store-only: Zustand 1,376,191 updates/s vs RTK 6,502 updates/s.
  • One surprise: almost all of RTK's store-only cost was Immer's auto-freeze on the 1,000-item state, not the Redux dev checks.
  • Not tested: Redux DevTools, RTK Query, createEntityAdapter, Zustand middleware (persist, immer, devtools), React Native, real browsers, SSR hydration.

How big was the React update gap?

Setup (1,000 rows, 300 updates)Median ms per updateRows re-rendered per update
Zustand useStore + selector0.1371
RTK default middleware + useSelector0.6761
RTK with immutable/serializable checks off0.6161
RTK with checks off and Immer autoFreeze off0.7581

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.

Why was the store-only gap so large?

Store only (Node, production)Updates per second
Zustand setState with spread1,376,191
RTK default middleware6,502
RTK with checks off6,657
RTK with checks off and Immer autoFreeze off386,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.

Does bundle size matter here?

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.

Should you switch from Redux Toolkit to Zustand?

SituationMy pick
New React app, mostly UI stateZustand
Existing RTK app with passing testsStay on RTK
Heavy server data fetching with cachingRTK Query, or a data-fetching library beside Zustand
Large team that wants one enforced patternRTK
High-frequency updates (drag, tickers, big tables)Zustand
Need DevTools time travel out of the boxRTK

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.

Sources

  • https://zustand.docs.pmnd.rs/
  • https://www.npmjs.com/package/zustand
  • https://redux-toolkit.js.org/
  • https://react-redux.js.org/
  • https://immerjs.github.io/immer/freezing
  • https://www.npmjs.com/package/@reduxjs/toolkit

Related

  • https://www.shoppercove.com/blog/jsdom-vs-happy-dom-vitest
  • https://www.shoppercove.com/blog/size-limit-vs-bundlesize
  • https://www.shoppercove.com/blog/knip-vs-depcheck
  • https://www.shoppercove.com/blog/jest-vs-vitest
  • https://www.shoppercove.com/blog/react-19-3-view-transitions-fragment-refs-2026
  • https://www.shoppercove.com/blog/vite-vs-parcel
  • https://www.shoppercove.com/blog/source-map-explorer-vs-webpack-bundle-analyzer
  • https://www.shoppercove.com/blog/zod-vs-valibot
zustandredux toolkitreact performancestate managementbenchmarkingreact 19immerbundle size

Lab evidence

What I found running this

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.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 48

    Zod vs Valibot: 1.14M vs 0.97M Parses

    zod 4.6.5 vs valibot 1.5.0 on one nested user schema (uuid/email/age/tags/profile/enum). Warm medians after 2k warmup + 9×50k: zod.parse 1,144,494 ops/s vs valibot.parse 968,187 (~1.18×). safeParse fail: valibot 697,760 vs zod 415,523 (~1.7×). esbuild minify+gzip9: 452,999/92,636 B vs 85,126/15,422 B. Package trees ~6.1 MB vs ~1.9 MB. Fixture /workspace/bench-zod-valibot/.

    5 Oct 2026

  • Plate 04

    Ajv vs Zod: 3.9M vs 1.1M Validations per Second

    ajv 8.20.0 (+ajv-formats 3.0.1) vs zod 4.6.5 on the same nested user shape as zod-vs-valibot. Warm medians (2k warmup + 9x50k): Ajv validate ok 3,933,337 ops/s vs zod.safeParse 1,138,406 (~3.5x). Bad input: Ajv allErrors 4,094,452 vs Zod 435,537 (~9.4x). Setup: Ajv new+addFormats+compile median 6.47 ms vs Zod build 0.17 ms; break-even ~10k validations. Browser gzip: Ajv runtime 41 KB, Ajv standalone 1.8 KB, Zod classic 93 KB, zod/mini 6.8 KB. Fixture /workspace/bench-ajv-zod/.

    5 Oct 2026

  • Plate 12

    Yarn Berry vs pnpm: 1.3s vs 0.24s Install

    Yarn 4.9.2 (Berry PnP + node-modules linker) vs pnpm 12.9.1 on one 22-dependency React+Vite app. Isolated caches (Yarn enableGlobalCache false; pnpm --store-dir). Warm medians (5): Yarn PnP 1.307 s, Yarn nm 2.471 s, pnpm 0.240 s. Lock+empty medians 1.656 / 2.789 / 0.725 s. Clean disk: Yarn PnP 246 MB; Yarn nm modules 189 MB; pnpm hardlink project 167 MB. Fixture /workspace/bench-yarn-pnpm/. Distinct from pnpm-vs-npm-vs-bun (no Yarn).

    5 Oct 2026

On this page

  1. What I tested
  2. How big was the React update gap?
  3. Why was the store-only gap so large?
  4. Does bundle size matter here?
  5. Should you switch from Redux Toolkit to Zustand?
  6. Sources
  7. Related
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove