ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 66

  1. Blog

Jotai vs Zustand: 0.15 ms vs 0.17 ms per Update

Aditya Challa·5 October 2026·5 min read

Hands-on
On this page
  1. What I tested
  2. How wide was the update gap?
  3. Does the bundle size change the call?
  4. Which one should you pick?
  5. Sources
  6. Related

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.

What I tested

  • Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 00:47–00:50 IST.
  • Versions: jotai 2.20.3, zustand 5.0.15, React 19.3.0, jsdom 27.4.0, Node 22.20.0, esbuild for the bundle check. NODE_ENV=production.
  • Fixture: /workspace/bench-jotai-zustand/. Normalized 1,000-item list. Zustand holds an items map and a bump action that replaces one entry. Jotai uses createStore plus 1,000 primitive atoms and store.set. Each row is a memoized component subscribed to one item (useStore selector or useAtomValue with an explicit store).
  • Timing: warm one update after mount, then 300 flushSync bumps × 7 trials. Also a burst of 300 sync bumps then one paint, and a store-only path of 20,000 bumps × 9 rounds with no React.
  • Findings: per-update median 0.145 ms (Jotai) vs 0.167 ms (Zustand); renders per update 1 for both. Burst of 300 updates 31.37 ms vs 39.11 ms. Store-only 915,340 vs 870,952 updates/s. Gzip 4,164 B vs 387 B.
  • One surprise: with flushSync before subscriptions had settled, Jotai looked like it re-rendered every row. After a short settle + one warm bump, isolation matched Zustand at 1 render per update. Measure after mount settles.
  • Not tested: jotai atomFamily / loadable, Zustand middleware (persist, immer), Redux Toolkit (covered separately), React Native, real browsers, SSR, DevTools.

How wide was the update gap?

PathMedian
Jotai, ms per flushSync update0.145
Zustand, ms per flushSync update0.167
Jotai, renders per update1
Zustand, renders per update1
Jotai, 300-update burst (ms)31.37
Zustand, 300-update burst (ms)39.11
Jotai, store-only updates/s915,340
Zustand, store-only updates/s870,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.

Does the bundle size change the call?

Browser build (esbuild minify, React external)Minifiedgzip -9
zustand create624 B387 B
jotai (atom, createStore, Provider, useAtomValue, useSetAtom)10,137 B4,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.

Which one should you pick?

SituationMy pick
One store, a few actions, smallest clientZustand
Many independent fields / derived atomsJotai
You already think in selectors over one treeZustand
You want atom graphs without boilerplateJotai
Bundle budget under 1 KB for stateZustand

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.

Sources

  • https://jotai.org/
  • https://www.npmjs.com/package/jotai
  • https://zustand.docs.pmnd.rs/
  • https://www.npmjs.com/package/zustand

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/biome-vs-eslint-prettier
  • https://www.shoppercove.com/blog/react-19-3-view-transitions-fragment-refs-2026
  • https://www.shoppercove.com/blog/msw-vs-nock
  • https://www.shoppercove.com/blog/oxlint-vs-eslint
jotaizustandreactbenchmarkingperformancestate managementbundle size

Lab evidence

What I found running this

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

Notes when a lab post goes up

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

Related links

  • Plate 60

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

    5 Oct 2026

  • Plate 38

    TanStack Query vs SWR: 10.4 KB vs 5.6 KB Gzip

    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 wide was the update gap?
  3. Does the bundle size change the call?
  4. Which one should you pick?
  5. Sources
  6. Related
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove