diff --git a/README.md b/README.md index 882a1e9..1397369 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,19 @@ With the Mutative middleware, you can simplify the handling of immutable data in `zustand-mutative` is 2-6x faster than zustand with spread operation, more than 10x faster than `zustand/middleware/immer`. [Read more about the performance comparison in Mutative](https://mutative.js.org/docs/getting-started/performance). +## Benchmarks + +Measure(ops/sec) to update 50K arrays and 1K objects, bigger is better([view source](https://github.com/unadlib/mutative/blob/main/test/performance/benchmark.ts)). [Mutative v1.1.0 vs Immer v10.1.1] + +![Benchmark](benchmark.jpg) + +``` +Zustand with Mutative - Update big array and object x 5,169 ops/sec ±2.09% (85 runs sampled) +Zustand with Immer - Update big array and object x 251 ops/sec ±0.40% (92 runs sampled) + +The fastest method is Zustand with Mutative - Update big array and object +``` + ## Installation In order to use the Mutative middleware in Zustand, you will need to install Mutative and Zustand as a direct dependency. @@ -49,7 +62,6 @@ export const useCountStore = create()( ); ``` - ### Mutative Options - [Strict mode](https://mutative.js.org/docs/advanced-guides/strict-mode) diff --git a/benchmark.jpg b/benchmark.jpg new file mode 100644 index 0000000..1342df6 Binary files /dev/null and b/benchmark.jpg differ diff --git a/package.json b/package.json index 53be7fc..fb237b1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "clean": "rimraf dist", "build": "yarn clean && tsc --skipLibCheck && yarn build:prod", "build:prod": "NODE_ENV=production rollup --config --bundleConfigAsCjs", - "commit": "yarn git-cz" + "commit": "yarn git-cz", + "benchmark": "NODE_ENV=production tsx scripts/benchmark.ts" }, "repository": { "type": "git", @@ -54,6 +55,7 @@ "@types/react": "^19.0.1", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", + "benchmark": "^2.1.4", "commitizen": "^4.3.0", "coveralls": "^3.1.1", "eslint": "^8.36.0", @@ -61,10 +63,12 @@ "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-prettier": "^4.2.1", + "immer": "^10.1.1", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mutative": "^1.1.0", "prettier": "^2.8.6", + "quickchart-js": "^3.1.3", "react": "^19.0.0", "react-dom": "^19.0.0", "rimraf": "^4.4.0", diff --git a/scripts/benchmark.ts b/scripts/benchmark.ts new file mode 100644 index 0000000..cd6fbbc --- /dev/null +++ b/scripts/benchmark.ts @@ -0,0 +1,180 @@ +import fs from 'fs'; +import https from 'https'; +import { Suite } from 'benchmark'; +import QuickChart from 'quickchart-js'; +import { create as createWithZustand } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import { mutative } from '../src'; + +const labels: string[] = []; +const result = [ + { + label: 'Zustand with Mutative', + backgroundColor: 'rgba(255, 0, 217, 0.5)', + data: [], + }, + { + label: 'Zustand with Immer', + backgroundColor: 'rgba(255, 0, 0, 0.5)', + data: [], + }, +]; + +interface Data { + arr: Record[]; + map: Record>; +} + +type Store = Data & { + update: () => void; +}; + +const getData = () => { + const baseState: Data = { + arr: [], + map: {}, + }; + + const createTestObject = () => + Array(10 * 5) + .fill(1) + .reduce((i, _, k) => Object.assign(i, { [k]: k }), {}); + + baseState.arr = Array(10 ** 4 * 5) + .fill('') + .map(() => createTestObject()); + + Array(10 ** 3) + .fill(1) + .forEach((_, i) => { + baseState.map[i] = { i }; + }); + return baseState; + // return deepFreeze(baseState); +}; + +let baseState: any; +let i: any; +let store: any; + +const suite = new Suite(); + +suite + .add( + 'Zustand with Mutative - Update big array and object', + () => { + store.getState().update(); + }, + { + onStart: () => { + i = Math.random(); + baseState = getData(); + store = createWithZustand( + mutative((set) => ({ + arr: baseState.arr, + map: baseState.map, + update: () => { + set((state) => { + state.arr.push(i); + state.map[i] = { i }; + }); + }, + })) + ); + }, + } + ) + .add( + 'Zustand with Immer - Update big array and object', + () => { + store.getState().update(); + }, + { + onStart: () => { + i = Math.random(); + baseState = getData(); + store = createWithZustand( + immer((set) => ({ + arr: baseState.arr, + map: baseState.map, + update: () => { + set((state) => { + state.arr.push(i); + state.map[i] = { i }; + }); + }, + })) + ); + }, + } + ) + .on('cycle', (event: any) => { + console.log(String(event.target)); + const [name, field = 'Update'] = event.target.name.split(' - '); + if (!labels.includes(field)) labels.push(field); + const item = result.find(({ label }) => label === name); + // @ts-ignore + item.data[labels.indexOf(field)] = Math.round(event.target.hz); + }) + .on('complete', function (this: any) { + console.log(`The fastest method is ${this.filter('fastest').map('name')}`); + }) + .run({ async: false }); + +try { + const config = { + type: 'horizontalBar', + data: { + labels, + datasets: result, + }, + options: { + title: { + display: true, + text: 'Zustand with Mutative vs Zustand vs Zustand with Immer - Performance', + }, + legend: { + position: 'bottom', + }, + elements: { + rectangle: { + borderWidth: 1, + }, + }, + scales: { + xAxes: [ + { + display: true, + scaleLabel: { + display: true, + fontSize: 10, + labelString: + 'Measure(ops/sec) to update 50K arrays and 1K objects, bigger is better.', + }, + }, + ], + }, + plugins: { + datalabels: { + anchor: 'center', + align: 'center', + font: { + size: 8, + }, + }, + }, + }, + }; + const chart = new QuickChart(); + chart.setConfig(config); + const file = fs.createWriteStream('benchmark.jpg'); + https.get(chart.getUrl(), (response) => { + response.pipe(file); + file.on('finish', () => { + file.close(); + console.log('update benchmark'); + }); + }); +} catch (err) { + console.error(err); +} diff --git a/yarn.lock b/yarn.lock index b27383c..b18c42f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1612,6 +1612,14 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +benchmark@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ== + dependencies: + lodash "^4.17.4" + platform "^1.3.3" + bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1956,6 +1964,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@^3.1.5: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" + integrity sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q== + dependencies: + node-fetch "^2.7.0" + cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3144,6 +3159,11 @@ ignore@^5.3.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +immer@^10.1.1: + version "10.1.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" + integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== + import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -3489,6 +3509,11 @@ jake@^10.8.5: filelist "^1.0.4" minimatch "^3.1.2" +javascript-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79" + integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg== + jest-changed-files@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" @@ -4089,7 +4114,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@4.17.21, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4337,6 +4362,13 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4598,6 +4630,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +platform@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" + integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== + possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" @@ -4686,6 +4723,14 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quickchart-js@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/quickchart-js/-/quickchart-js-3.1.3.tgz#7352df8e35b66ce4f50d6ea51252cee2e5213962" + integrity sha512-QzPUXBA/UntYBbOMITtMz7B426fes1XFmmjmjA070jXeMWhyhDojJf2aSZPsekj35ywfJhWjY6TKf3S0/XxyAg== + dependencies: + cross-fetch "^3.1.5" + javascript-stringify "^2.1.0" + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -5329,6 +5374,11 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -5663,6 +5713,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -5688,6 +5743,14 @@ whatwg-url@^11.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"