5 min read
I bumped one row in a 1,000-row React 19 list on this box: Jotai 2.20.3 median 0.145 ms per update, Zustand 5.0.15 median 0.167 ms, both with exactly one re-render per bump. The bundle gap is the bigger story — jotai core gzip 4,164 B against zustand create at 387 B.
Short answer: pick Zustand for a single store and the smallest client. Pick Jotai when you want per-field atoms and derived graphs without inventing selectors. No affiliate links in this post.
| Path | Median |
|---|---|
| Jotai, ms per flushSync update | 0.145 |
| Zustand, ms per flushSync update | 0.167 |
| Jotai, renders per update | 1 |
| Zustand, renders per update | 1 |
| Jotai, 300-update burst (ms) | 31.37 |
| Zustand, 300-update burst (ms) | 39.11 |
| Jotai, store-only updates/s | 915,340 |
| Zustand, store-only updates/s | 870,952 |
On this fixture the libraries are within noise on React update cost. Jotai was a bit ahead on the burst and store-only loops. Isolation worked the same: touching one atom or one selector re-rendered one row. The jsdom harness details sit next to my jsdom vs happy-dom notes.
| Browser build (esbuild minify, React external) | Minified | gzip -9 |
|---|---|---|
| zustand create | 624 B | 387 B |
| jotai (atom, createStore, Provider, useAtomValue, useSetAtom) | 10,137 B | 4,164 B |
Zustand was about 10× smaller gzip on the imports I actually used. Disk trees were about 288 KB for zustand against 1.4 MB for jotai. Track shipped weight with size-limit vs bundlesize. If you are choosing between Zustand and Redux Toolkit instead, that is a different intent (single store vs Redux store + Immer) and belongs in its own comparison once published.
| Situation | My pick |
|---|---|
| One store, a few actions, smallest client | Zustand |
| Many independent fields / derived atoms | Jotai |
| You already think in selectors over one tree | Zustand |
| You want atom graphs without boilerplate | Jotai |
| Bundle budget under 1 KB for state | Zustand |
Both stayed at one re-render per bump once the tree had settled. Jotai's model shines when atoms depend on atoms; Zustand's when the app state is already a tree you are happy to slice. Lint and unused-export cleanup after a swap: Biome vs ESLint/Prettier and knip vs depcheck. Test runner for the suite: Jest vs Vitest.
Bottom line: on a 1,000-row React 19 list, Jotai 2.20.3 took 0.15 ms per update against 0.17 ms for Zustand 5.0.15, both with one re-render, while Zustand shipped 0.4 KB gzip against Jotai's 4.2 KB. Who should not switch: teams whose Zustand stores are already simple and under budget — the 0.02 ms gap is not a reason to rewrite.
How this was made: I installed Jotai and Zustand on the ShopperCove box, timed React update and store-only loops under jsdom, measured esbuild minify+gzip 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:47-00:50 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). jotai 2.20.3 vs zustand 5.0.15, React 19.3.0, jsdom 27.4.0. Fixture /workspace/bench-jotai-zustand/: 1,000-item Zustand map with bump-one-entry action vs createStore + 1,000 primitive atoms; memoized row components (useStore selector / useAtomValue with store option). After mount settle + one warm bump: 300 flushSync updates x 7 trials. Medians: Jotai 0.145 ms/update, Zustand 0.167 ms/update; renders/update 1 both. Burst 300: 31.37 vs 39.11 ms, 300 renders each. Store-only 20k x 9: 915,340 vs 870,952 ups. esbuild minify/gzip9 React external: jotai 10,137/4,164 B; zustand 624/387 B. Disk ~1.4 MB vs ~288 KB. Raw: res-react-final.json, res-store.json, res-bundle.json. Not tested: atomFamily/loadable, Zustand middleware, RTK, RN, real browsers, SSR, DevTools. No affiliate. Distinct from zustand-vs-redux-toolkit (atoms vs single store, not Redux).
Hands-on6 Oct 20264 min read
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/.
Hands-on6 Oct 20265 min read
Hands-on6 Oct 20264 min read