diff --git a/README.md b/README.md index 0dfcddf..50f2988 100644 --- a/README.md +++ b/README.md @@ -88,34 +88,19 @@ sequenceDiagram ## Performance +Measure(ops/sec) to update 50K arrays and 1K objects, bigger is better([view source](./scripts/benchmark.ts)). [Coaction v0.1.5 vs Zustand v5.0.2] + ![Benchmark](benchmark.jpg) -Measure(ops/sec) to update 10K arrays, bigger is better([view source](https://github.com/unadlib/mutative/blob/main/test/performance/benchmark.ts)). - -| Library | Test Case | Ops/sec | -| -------------- | ------------------------------- | ------- | -| @coaction/mobx | bigInitWithoutRefsWithoutAssign | 37.44 | -| mobx | bigInitWithoutRefsWithoutAssign | 37.67 | -| coaction | bigInitWithoutRefsWithoutAssign | 18,809 | -| mobx-keystone | bigInitWithoutRefsWithoutAssign | 8.53 | -| @coaction/mobx | bigInitWithoutRefsWithAssign | 1.54 | -| mobx | bigInitWithoutRefsWithAssign | 10.78 | -| coaction | bigInitWithoutRefsWithAssign | 45.20 | -| mobx-keystone | bigInitWithoutRefsWithAssign | 0.13 | -| @coaction/mobx | bigInitWithRefsWithoutAssign | 14.99 | -| mobx | bigInitWithRefsWithoutAssign | 16.68 | -| coaction | bigInitWithRefsWithoutAssign | 255 | -| mobx-keystone | bigInitWithRefsWithoutAssign | 2.35 | -| @coaction/mobx | bigInitWithRefsWithAssign | 1.01 | -| mobx | bigInitWithRefsWithAssign | 7.71 | -| coaction | bigInitWithRefsWithAssign | 57.22 | -| mobx-keystone | bigInitWithRefsWithAssign | 0.11 | -| @coaction/mobx | init | 38.57 | -| mobx | init | 43.88 | -| coaction | init | 8,523 | -| mobx-keystone | init | 41.19 | - -This table benchmarks various state management libraries on large initialization tasks. Coaction stands out dramatically, performing at least hundreds of times faster in certain scenarios. For example, in the “bigInitWithoutRefsWithoutAssign” test, Coaction achieves 18,809 ops/sec compared to MobX’s 37.67 ops/sec—over 500 times faster. Similarly, in the “init” test, Coaction reaches 8,523 ops/sec versus MobX’s 43.88 ops/sec—an increase of roughly 200 times. Additionally, Coaction consistently outperforms other libraries across various initialization scenarios, showcasing its exceptional efficiency in handling large-scale data initialization. These results highlight Coaction’s superior performance and make it a highly effective solution for managing complex state in modern front-end applications. +``` +Coaction x 4,837 ops/sec ±3.79% (65 runs sampled) +Coaction with Mutative x 4,276 ops/sec ±3.04% (85 runs sampled) +Zustand x 4,753 ops/sec ±3.52% (75 runs sampled) +Zustand with Immer x 251 ops/sec ±0.26% (93 runs sampled) +Zustand with Mutative x 4,292 ops/sec ±3.30% (72 runs sampled) + +The fastest method is Coaction,Zustand +``` > We will also provide more complete benchmarking. diff --git a/benchmark.jpg b/benchmark.jpg index af5d4c0..b7b72fa 100644 Binary files a/benchmark.jpg and b/benchmark.jpg differ diff --git a/package.json b/package.json index 6448465..38258b2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "version": "changeset version && node ./scripts/bump-peer-dep-ranges.js", "publish": "lerna exec --no-private --no-bail -- npm publish --access=public", "update:version": "lerna version --amend --no-git-tag-version", - "benchmark": "tsx ./packages/coaction-mobx/test/benchmark/index.ts" + "benchmark:mobx": "tsx ./packages/coaction-mobx/test/benchmark/index.ts", + "benchmark": "NODE_ENV='production' tsx ./scripts/benchmark.ts" }, "authors": [ "Michael Lin (https://github.com/unadlib)" @@ -80,6 +81,7 @@ "commitizen": "^4.3.0", "cz-conventional-changelog": "^3.3.0", "husky": "^3.1.0", + "immer": "^10.1.1", "jest": "^29.7.0", "jest-config": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -95,7 +97,8 @@ "tsx": "^4.19.2", "typescript": "^5.6.3", "vue": "^3.0.11", - "webpack-dev-middleware": "^3.6.0" + "webpack-dev-middleware": "^3.6.0", + "zustand-mutative": "^1.2.0" }, "config": { "commitizen": { diff --git a/packages/coaction-mobx/README.md b/packages/coaction-mobx/README.md index 7573d1e..2ed4532 100644 --- a/packages/coaction-mobx/README.md +++ b/packages/coaction-mobx/README.md @@ -51,6 +51,39 @@ store.setState(() => { }); ``` +## Performance + +![Benchmark](./benchmark.jpg) + +Measure(ops/sec) to update 10K arrays, bigger is better([view source](https://github.com/unadlib/mutative/blob/main/test/performance/benchmark.ts)). + +| Library | Test Case | Ops/sec | +| -------------- | ------------------------------- | ------- | +| @coaction/mobx | bigInitWithoutRefsWithoutAssign | 37.44 | +| mobx | bigInitWithoutRefsWithoutAssign | 37.67 | +| coaction | bigInitWithoutRefsWithoutAssign | 18,809 | +| mobx-keystone | bigInitWithoutRefsWithoutAssign | 8.53 | +| @coaction/mobx | bigInitWithoutRefsWithAssign | 1.54 | +| mobx | bigInitWithoutRefsWithAssign | 10.78 | +| coaction | bigInitWithoutRefsWithAssign | 45.20 | +| mobx-keystone | bigInitWithoutRefsWithAssign | 0.13 | +| @coaction/mobx | bigInitWithRefsWithoutAssign | 14.99 | +| mobx | bigInitWithRefsWithoutAssign | 16.68 | +| coaction | bigInitWithRefsWithoutAssign | 255 | +| mobx-keystone | bigInitWithRefsWithoutAssign | 2.35 | +| @coaction/mobx | bigInitWithRefsWithAssign | 1.01 | +| mobx | bigInitWithRefsWithAssign | 7.71 | +| coaction | bigInitWithRefsWithAssign | 57.22 | +| mobx-keystone | bigInitWithRefsWithAssign | 0.11 | +| @coaction/mobx | init | 38.57 | +| mobx | init | 43.88 | +| coaction | init | 8,523 | +| mobx-keystone | init | 41.19 | + +This table benchmarks various state management libraries on large initialization tasks. Coaction stands out dramatically, performing at least hundreds of times faster in certain scenarios. For example, in the “bigInitWithoutRefsWithoutAssign” test, Coaction achieves 18,809 ops/sec compared to MobX’s 37.67 ops/sec—over 500 times faster. Similarly, in the “init” test, Coaction reaches 8,523 ops/sec versus MobX’s 43.88 ops/sec—an increase of roughly 200 times. Additionally, Coaction consistently outperforms other libraries across various initialization scenarios, showcasing its exceptional efficiency in handling large-scale data initialization. These results highlight Coaction’s superior performance and make it a highly effective solution for managing complex state in modern front-end applications. + +> We will also provide more complete benchmarking. + ## Documentation You can find the documentation [here](https://github.com/unadlib/coaction). diff --git a/packages/coaction-mobx/benchmark.jpg b/packages/coaction-mobx/benchmark.jpg new file mode 100644 index 0000000..af5d4c0 Binary files /dev/null and b/packages/coaction-mobx/benchmark.jpg differ diff --git a/packages/coaction-mobx/test/benchmark/index.ts b/packages/coaction-mobx/test/benchmark/index.ts index 3cb0aaf..10b9ccb 100644 --- a/packages/coaction-mobx/test/benchmark/index.ts +++ b/packages/coaction-mobx/test/benchmark/index.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import https from 'https'; +import path from 'path'; import { Suite } from 'benchmark'; import QuickChart from 'quickchart-js'; import { createCoactionMobxStore } from './coaction-mobx'; @@ -316,7 +317,9 @@ try { const chart = new QuickChart(); chart.setConfig(config); console.log('config:', JSON.stringify(config)); - const file = fs.createWriteStream('benchmark.jpg'); + const file = fs.createWriteStream( + path.resolve(__dirname, '../../benchmark.jpg') + ); https.get(chart.getUrl(), (response) => { response.pipe(file); file.on('finish', () => { diff --git a/packages/core/src/handleState.ts b/packages/core/src/handleState.ts index 8d89485..6541851 100644 --- a/packages/core/src/handleState.ts +++ b/packages/core/src/handleState.ts @@ -45,37 +45,14 @@ export const handleState = ( : merge; const enablePatches = store.transport ?? (options as StoreOptions).enablePatches; - if (!enablePatches) { - if (internal.mutableInstance) { - if (internal.actMutable) { - internal.actMutable(() => { - fn.apply(null); - }); - return []; - } - fn.apply(null); + if (!enablePatches && internal.mutableInstance) { + if (internal.actMutable) { + internal.actMutable(() => { + fn.apply(null); + }); return []; } - // best performance by default for immutable state - // TODO: supporting nested set functions? - try { - internal.backupState = internal.rootState; - internal.rootState = createWithMutative( - internal.rootState, - (draft) => { - internal.rootState = draft as Draft; - return fn.apply(null); - } - ); - } catch (error) { - internal.rootState = internal.backupState; - throw error; - } - if (internal.updateImmutable) { - internal.updateImmutable(internal.rootState as T); - } else { - internal.listeners.forEach((listener) => listener()); - } + fn.apply(null); return []; } internal.backupState = internal.rootState; @@ -122,6 +99,45 @@ export const handleState = ( throw new Error('setState cannot be called within the updater'); } internal.isBatching = true; + if ( + !store.share && + !(options as StoreOptions).enablePatches && + !internal.mutableInstance + ) { + if (typeof next === 'function') { + try { + internal.backupState = internal.rootState; + internal.rootState = createWithMutative( + internal.rootState, + (draft) => { + internal.rootState = draft as Draft; + return next(internal.module); + } + ); + } catch (error) { + internal.rootState = internal.backupState; + internal.isBatching = false; + throw error; + } + } else { + const copy = {} as T; + const rootState = internal.rootState as T; + for (const key of Object.keys(rootState)) { + copy[key] = rootState[key]; + } + for (const key of Object.keys(next!)) { + copy[key] = next![key]; + } + internal.rootState = copy; + } + if (internal.updateImmutable) { + internal.updateImmutable(internal.rootState as T); + } else { + internal.listeners.forEach((listener) => listener()); + } + internal.isBatching = false; + return []; + } let result: void | [] | [any, Patches, Patches]; try { const isDrafted = internal.mutableInstance && isDraft(internal.rootState); diff --git a/packages/core/test/middleware.test.ts b/packages/core/test/middleware.test.ts index dd476f2..abe5de3 100644 --- a/packages/core/test/middleware.test.ts +++ b/packages/core/test/middleware.test.ts @@ -78,42 +78,42 @@ test('base', () => { ] `); expect(stateFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 1, - 1, - 1, - 1, - ], - [ - 1, - 1, - 1, - ], - ] - `); + 1, + 1, + 1, + 1, + ], + [ + 1, + 1, + 1, + ], +] +`); expect(getterFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 2, - 2, - 2, - 2, - ], - [ - 2, - 2, - 2, - ], - ] - `); + 2, + 2, + 2, + 2, + ], + [ + 2, + 2, + 2, + ], +] +`); expect(useStore.getState()).toMatchInlineSnapshot(` - { - "count": 1, - "double": 2, - "increment": [Function], - } - `); +{ + "count": 1, + "double": 2, + "increment": [Function], +} +`); increment(); expect(logFn.mock.calls).toMatchInlineSnapshot(` [ @@ -136,62 +136,62 @@ test('base', () => { ] `); expect(stateFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 1, - 1, - 1, - 1, - ], - [ - 1, - 1, - 1, - ], - [ - 2, - 2, - 2, - 2, - ], - [ - 2, - 2, - 2, - ], - ] - `); + 1, + 1, + 1, + 1, + ], + [ + 1, + 1, + 1, + ], + [ + 2, + 2, + 2, + 2, + ], + [ + 2, + 2, + 2, + ], +] +`); expect(getterFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 2, - 2, - 2, - 2, - ], - [ - 2, - 2, - 2, - ], - [ - 4, - 4, - 4, - 4, - ], - [ - 4, - 4, - 4, - ], - ] - `); + 2, + 2, + 2, + 2, + ], + [ + 2, + 2, + 2, + ], + [ + 4, + 4, + 4, + 4, + ], + [ + 4, + 4, + 4, + ], +] +`); expect(useStore.getState()).toMatchInlineSnapshot(` - { - "count": 2, - "double": 4, - "increment": [Function], - } - `); +{ + "count": 2, + "double": 4, + "increment": [Function], +} +`); }); diff --git a/packages/logger/test/middleware.test.ts b/packages/logger/test/middleware.test.ts index 49bb343..38fc056 100644 --- a/packages/logger/test/middleware.test.ts +++ b/packages/logger/test/middleware.test.ts @@ -51,101 +51,101 @@ test('base', () => { useStore.getState().increment(); expect(logFn.mock.calls).toMatchInlineSnapshot(`[]`); expect(stateFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 1, - 1, - 1, - 1, - ], - [ - 1, - 1, - 1, - ], - ] - `); + 1, + 1, + 1, + 1, + ], + [ + 1, + 1, + 1, + ], +] +`); expect(getterFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 2, - 2, - 2, - 2, - ], - [ - 2, - 2, - 2, - ], - ] - `); + 2, + 2, + 2, + 2, + ], + [ + 2, + 2, + 2, + ], +] +`); expect(useStore.getState()).toMatchInlineSnapshot(` - { - "count": 1, - "double": 2, - "increment": [Function], - } - `); +{ + "count": 1, + "double": 2, + "increment": [Function], +} +`); increment(); expect(logFn.mock.calls).toMatchInlineSnapshot(`[]`); expect(stateFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 1, - 1, - 1, - 1, - ], - [ - 1, - 1, - 1, - ], - [ - 2, - 2, - 2, - 2, - ], - [ - 2, - 2, - 2, - ], - ] - `); + 1, + 1, + 1, + 1, + ], + [ + 1, + 1, + 1, + ], + [ + 2, + 2, + 2, + 2, + ], + [ + 2, + 2, + 2, + ], +] +`); expect(getterFn.mock.calls).toMatchInlineSnapshot(` +[ [ - [ - 2, - 2, - 2, - 2, - ], - [ - 2, - 2, - 2, - ], - [ - 4, - 4, - 4, - 4, - ], - [ - 4, - 4, - 4, - ], - ] - `); + 2, + 2, + 2, + 2, + ], + [ + 2, + 2, + 2, + ], + [ + 4, + 4, + 4, + 4, + ], + [ + 4, + 4, + 4, + ], +] +`); expect(useStore.getState()).toMatchInlineSnapshot(` - { - "count": 2, - "double": 4, - "increment": [Function], - } - `); +{ + "count": 2, + "double": 4, + "increment": [Function], +} +`); }); diff --git a/scripts/benchmark.ts b/scripts/benchmark.ts new file mode 100644 index 0000000..b9ac151 --- /dev/null +++ b/scripts/benchmark.ts @@ -0,0 +1,261 @@ +import fs from 'fs'; +import https from 'https'; +import { Suite } from 'benchmark'; +import QuickChart from 'quickchart-js'; +import { create as createWithCoaction } from 'coaction'; +import { create as createWithZustand } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import { mutative } from 'zustand-mutative'; + +const labels: string[] = []; +const result = [ + { + label: 'Coaction', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + data: [] + }, + { + label: 'Zustand', + backgroundColor: 'rgba(0, 255, 0, 0.5)', + data: [] + }, + { + label: 'Zustand with Immer', + backgroundColor: 'rgba(255, 0, 0, 0.5)', + data: [] + }, + { + label: 'Zustand with Mutative', + backgroundColor: 'rgba(255, 0, 217, 0.5)', + data: [] + }, + { + label: 'Coaction with Mutative', + backgroundColor: 'rgba(255, 120, 120, 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( + 'Coaction', + () => { + store.getState().update(); + }, + { + onStart: () => { + i = Math.random(); + baseState = getData(); + store = createWithCoaction((set, get) => ({ + arr: baseState.arr, + map: baseState.map, + update: () => { + set({ + arr: [...get().arr, i], + map: { ...get().map, [i]: { i } } + }); + } + })); + } + } + ) + .add( + 'Coaction with Mutative', + () => { + store.getState().update(); + }, + { + onStart: () => { + i = Math.random(); + baseState = getData(); + store = createWithCoaction((set, get) => ({ + arr: baseState.arr, + map: baseState.map, + update: () => { + set((state) => { + state.arr.push(i); + state.map[i] = { i }; + }); + } + })); + } + } + ) + .add( + 'Zustand', + () => { + store.getState().update(); + }, + { + onStart: () => { + i = Math.random(); + baseState = getData(); + store = createWithZustand((set, get) => ({ + arr: baseState.arr, + map: baseState.map, + update: () => + set({ + arr: [...get().arr, i], + map: { ...get().map, [i]: { i } } + }) + })); + } + } + ) + .add( + 'Zustand with Immer', + () => { + 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 }; + }); + } + })) + ); + } + } + ) + .add( + 'Zustand with Mutative', + () => { + 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 }; + }); + } + })) + ); + } + } + ) + .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: 'Coaction 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 2c721b7..0203b08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6014,6 +6014,11 @@ ignore@^5.0.4, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== +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@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" @@ -11382,6 +11387,11 @@ zod@^3.21.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== +zustand-mutative@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/zustand-mutative/-/zustand-mutative-1.2.0.tgz#550c917c0e10cf02f83b0ef561908aba40457245" + integrity sha512-6TIfG4iXlrftnrmfpuxpPyqoybyTIuZRG9aKO+h+mTiNiZ55rxIoHoQe7v4eDe+lAd0enFNWer9ZvKQhOb25HQ== + zustand@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.2.tgz#f7595ada55a565f1fd6464f002a91e701ee0cfca"