Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![NPM Downloads](https://img.shields.io/npm/dm/mutative)](https://npmtrends.com/mutative)
![license](https://img.shields.io/npm/l/mutative)

**Mutative** - A JavaScript library for efficient immutable updates. In the [benchmark](./perf-testing/reports/SUMMARY.md), it was about 3.6x faster than Immer with the same settings and 6.6x faster with each library's defaults across 93 workloads.
**Mutative** - A JavaScript library for efficient immutable updates. In the [benchmark](./perf-testing/reports/SUMMARY.md), it was about 3.7x faster than Immer with the same settings and 6.8x faster with each library's defaults across 93 workloads.

In that benchmark the gap widened on large arrays, where Mutative moved elements up to 1,213x faster than Immer. When copying dominates, such as updating objects with thousands of keys, inserting at the front of a large array or adding an item to a large Set, Mutative is even faster than hand-written reducers.

Expand All @@ -35,7 +35,7 @@ This is why Mutative was created.

The [benchmark suite](./perf-testing/README.md) times 93 workloads: Immer's own performance tests, array methods, reads, Map and Set values, object records, class instances, a deep path, patch application, returned values, and searches. It compares Mutative with Immer 11.1.18 and with reducers written by hand, after checking every result against those reducers. The [performance summary](./perf-testing/reports/SUMMARY.md) has the complete results, the method, and their limits.

With matched settings, both freezing or both not and both generating patches or both not, Mutative was faster than Immer in 548 of 566 measured cases, 3.6x on geometric mean. With each library's defaults, Mutative without auto-freeze and Immer with it, Mutative was faster in 142 of 145 cases, 6.6x on geometric mean.
With matched settings, both freezing or both not and both generating patches or both not, Mutative was faster than Immer in 551 of 566 measured cases, 3.7x on geometric mean. With each library's defaults, Mutative without auto-freeze and Immer with it, Mutative was faster in 142 of 145 cases, 6.8x on geometric mean.

Times are microseconds per update, medians of three runs on an Apple M1 Max with Node.js 24.16.0, each library in a process of its own; lower is better. Mutative, the first Immer column, and the hand-written reducers run without auto-freeze; the second Immer column shows Immer's default.

Expand All @@ -53,25 +53,25 @@ Times are microseconds per update, medians of three runs on an Apple M1 Max with
| Add a number to a Set | 10,000 | 34.1 | 929 | 1,004 | 66.0 |
| Update a class instance | 10,000 | 2.92 | 3.43 | 668 | 1.43 |
| Update a value ten levels deep | — | 3.47 | 4.46 | 7.83 | 0.75 |
| Apply patches to 10% of rows | 10,000 | 1,440 | 1,182 | 2,110 | — |
| Apply patches to 10% of rows | 10,000 | 736 | 1,169 | 2,102 | — |
| Search `current()` after `shift()` | 10,000 | 257 | 11,230 | 11,913 | 64.7 |
| Return draft.filter() | 10,000 | 2,647 | 4,306 | 4,735 | 78.8 |
| Return a new state | 10,000 | 2,594 | 5,628 | 0.79 | 0.02 |
| Return it with rawReturn() | 10,000 | 0.26 | 5,699 | 0.79 | 0.02 |

Each library ran each workload in a process of its own. With all of them in one process every library ran slower, and not by the same amount: Mutative and Immer took 1.1 times as long on geometric mean, and the hand-written reducers 1.7 times. The search removes the first row with `shift()`, then finds the last row in a snapshot of the array and updates it. Immer has no `rawReturn()` and returns the same plain value in the last two rows; with auto-freeze, Mutative skips a returned state that is already frozen, as Immer does, taking 0.53 µs. Immer was faster in 6 of the 566 matched cases: when applying patches that replace nested values, by 16-22%; with auto-freeze, when updating a class instance with 1,000 fields; and in one concatenation with auto-freeze and patches, by 8%. The Set workloads were measured again after Set drafts began to change their copy directly; adding or deleting a Set item became 3-50x faster.
Each library ran each workload in a process of its own. With all of them in one process every library ran slower, and not by the same amount: Mutative and Immer took 1.1 times as long on geometric mean, and the hand-written reducers 1.7 times. The search removes the first row with `shift()`, then finds the last row in a snapshot of the array and updates it. Immer has no `rawReturn()` and returns the same plain value in the last two rows; with auto-freeze, Mutative skips a returned state that is already frozen, as Immer does, taking 0.53 µs. Immer was faster in 3 of the 566 matched cases: with auto-freeze, when updating a class instance with 1,000 fields; and in one concatenation with auto-freeze and patches, by 8%. The Set workloads were measured again after Set drafts began to change their copy directly; adding or deleting a Set item became 3-50x faster. The patch application workloads were measured again after `apply()` began to read the type of each draft from its state; applying patches that replace nested values became 1.6-2x faster.

Run `pnpm benchmark:immer` to measure the suite; the [benchmark guide](./perf-testing/README.md) describes its options.

### Large arrays

At 1,000 and 10,000 rows the gap grows. Against Immer without its array-method plugin, Mutative was faster in 190 of 200 cases at these sizes, 4.9x on geometric mean, and faster in every case that moves elements:
At 1,000 and 10,000 rows the gap grows. Against Immer without its array-method plugin, Mutative was faster in 192 of 200 cases at these sizes, 5.0x on geometric mean, and faster in every case that moves elements:

| Auto-freeze | Patches | All workloads, 1,000 rows | All workloads, 10,000 rows | Moves, 1,000 rows | Moves, 10,000 rows |
| ----------- | ------- | ------------------------: | -------------------------: | ----------------: | -----------------: |
| off | off | 8.8x | 10.9x | 211x | 442x |
| off | off | 9.2x | 11.3x | 211x | 442x |
| off | on | 4.6x | 5.2x | 7.1x | 7.5x |
| on | off | 3.9x | 4.2x | 32x | 40x |
| on | off | 4.0x | 4.4x | 32x | 40x |
| on | on | 2.8x | 3.0x | 6.6x | 7.1x |

Each value is the geometric mean of Immer's time over Mutative's; moves are `shift`, `unshift`, `splice` insertion, and `reverse`. Mutative moves elements natively on its copy, while Immer moves each one through its draft proxy: removing the first of 10,000 rows took 7.10 µs against 8,605 µs, 1,213x. With patches, both libraries emit one patch per moved index, which bounds the gain to 5-10x. The [performance summary](./perf-testing/reports/SUMMARY.md) breaks these results down by scenario.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"name": "ESM, all exports",
"path": "dist/mutative.esm.mjs",
"import": "*",
"limit": "8.4 KB"
"limit": "8.5 KB"
}
],
"repository": {
Expand Down
Loading
Loading