Mutative

Performance

Benchmark results against Immer and hand-written reducers across 97 workloads, and how to run the benchmarks.

Mutative is about 3.6x faster than Immer with matched settings and 6.6x faster with each library's defaults, across 97 benchmarked workloads. A hand-written reducer that copies only what it changes is faster still in most of them.

Benchmark suite

The benchmark suite times 99 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. Its last full run measured 97 of them, and a focused run the two that return fresh data. It compares Mutative with Immer 11.1.18 and with reducers written by hand, after checking every result against those reducers. The performance summary has the complete results, the method, and their limits.

Mutative vs Immer

Mutative passed all of Immer's test cases.

With matched settings, both freezing or both not and both generating patches or both not, Mutative was faster than Immer in 595 of 614 measured cases, 3.6x on geometric mean. With each library's defaults, Mutative without auto-freeze and Immer with it, Mutative was faster in 154 of 157 cases, 6.6x on geometric mean. The other 3 return a state that Immer froze earlier: Immer skips frozen values when it searches a returned state for drafts, while Mutative, which does not freeze by default, searches it unless the recipe wraps it in rawReturn(), which took 0.26 µs.

The overall comparison

97 workloads · geometric mean of Immer / Mutative time

Matched settings

Same auto-freeze and patch settings

3.6×faster on geometric mean

595 / 614 cases favor Mutative

  • Mutative faster: 595
  • Within 5%: 13
  • Immer faster: 6

Each library’s defaults

Only Immer auto-freezes; patches off

6.6×faster on geometric mean

154 / 157 cases favor Mutative

  • Mutative faster: 154
  • Within 5%: 0
  • Immer faster: 3
Each card covers a different set of measured cases. Faster means more than 5% less time; the factors are geometric means, not a speedup guaranteed for every workload.

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.

WorkloadRowsMutativeImmerImmer, auto-freezeHand-written
Update one field of a small object—0.510.690.950.02
Update an array item found by ID1001.121.8225.00.38
200 RTK Query-style updates—1,0011,1334,795352
Read every row by index10,0004,7569,5849,68757.7
Remove the first row10,0007.047,4088,1371.40
Update every row10,0006,46212,57015,607211
Update one of 10,000 records10,0001,2002,0943,1312,088
Insert into a 1,000-property object—50.3149229126
Update a Map value10,000518526733519
Add a number to a Set10,00029.590497561.2
Update a class instance10,0002.923.446681.42
Update a value ten levels deep—3.504.417.840.76
Apply patches to 10% of rows10,0007341,1662,095—
Search current() after shift()10,00025711,25511,84664.8
Return draft.filter()10,0002,6584,2514,73677.7
Return a new state10,0002,6045,7180.790.02
Return it with rawReturn()10,0000.265,7030.790.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 took 1.1 times as long on geometric mean, Immer 1.2 times, 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 614 matched cases, all with auto-freeze: four updates of class instances with 100 to 10,000 fields, and two array updates at 100 rows whose runs overlapped those of Mutative.

Large arrays

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

How the gap changes with array size

Immer / Mutative time · higher is better for Mutative

All large-array workloadsLog scale
Large-array speedups: all workloadsFreeze off, patches off: 8.7× at 1,000 rows and 10.8× at 10,000 rows. Freeze off, patches on: 4.1× at 1,000 rows and 4.5× at 10,000 rows. Freeze on, patches off: 3.9× at 1,000 rows and 4.2× at 10,000 rows. Freeze on, patches on: 2.7× at 1,000 rows and 2.8× at 10,000 rows.1×2×5×10×20×1,000 rows10,000 rowsFreeze off, patches off · 1,000 rows: 8.7×Freeze off, patches off · 10,000 rows: 10.8×Freeze off, patches on · 1,000 rows: 4.1×Freeze off, patches on · 10,000 rows: 4.5×Freeze on, patches off · 1,000 rows: 3.9×Freeze on, patches off · 10,000 rows: 4.2×Freeze on, patches on · 1,000 rows: 2.7×Freeze on, patches on · 10,000 rows: 2.8×
  • Freeze off · patches off

    8.7× → 10.8× at 1k → 10k rows

  • Freeze off · patches on

    4.1× → 4.5× at 1k → 10k rows

  • Freeze on · patches off

    3.9× → 4.2× at 1k → 10k rows

  • Freeze on · patches on

    2.7× → 2.8× at 1k → 10k rows

Both libraries use the same settings. The vertical axis is logarithmic; 1× means equal time. Element moves cover shift, unshift, splice insertion, and reverse. Immer’s array-method plugin is off.
Auto-freezePatchesAll workloads, 1,000 rowsAll workloads, 10,000 rowsMoves, 1,000 rowsMoves, 10,000 rows
offoff8.7x10.8x209x430x
offon4.1x4.5x7.1x7.3x
onoff3.9x4.2x32x39x
onon2.7x2.8x6.6x6.8x

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.04 µs against 7,408 µs, 1,052x. With patches, both libraries emit one patch per moved index, which bounds the gain to 5-9x. The performance summary breaks these results down by scenario.

With patches

With patches on and auto-freeze off, Mutative was faster than Immer in 148 of 150 cases and within 5% in the other 2, 3.3x on geometric mean, and faster than Mutative 1.3.0 in 131 and within 5% in the rest, 4.6x. In every array case measured it was faster than both: 4.4x Immer and 13x Mutative 1.3.0 on geometric mean. Patches cost little when an update changes a few paths: pushing a row and inserting a property at 10,000 rows took 63.2 µs with patches against 63.8 µs without. Moving elements emits one patch per moved index in every library, so removing the first of 10,000 rows produces 10,000 forward and 10,000 inverse patches.

Times are microseconds per update with patches on and auto-freeze off:

The cost of updates with patches

Microseconds per update · shorter is better

Push a row and insert a property

5.7× faster than Immer

Mutative63.2 µs
Immer360 µs

Remove the first row with shift

8.41× faster than Immer

Mutative1,063 µs
Immer8,943 µs

Update every row

2.54× faster than Immer

Mutative8,555 µs
Immer21,767 µs

Update one of 10,000 records

1.78× faster than Immer

Mutative1,184 µs
Immer2,104 µs
Patches on, auto-freeze off, 10,000 rows. Each workload has its own linear scale. Medians of three runs on Apple M1 Max, Node.js 24.16.0; Immer 11.1.18 with its array-method plugin off.
WorkloadRowsMutativeImmerMutative 1.3.0
Push a row and insert a property10,00063.2360210
Update an array item found by ID1001.503.223.35
200 RTK Query-style updates—1,1401,4702,116
Remove the first row with splice10011.583.62,982
Insert a row in the middle1008.8348.01,191
Sort rows1001051292,466
Remove the first row with shift10,0001,0638,943265,439
Reverse the rows10,0001,0719,424280,542
Update every row10,0008,55521,76722,549
Update one of 10,000 records10,0001,1842,1041,181

Immer's optional enableArrayMethods() plugin also runs array methods on the draft's copy, and with patches it comes within 1.13-1.70x of Mutative when moving elements of 1,000 or 10,000 rows. These comparisons leave it off because in Immer 11.1.18 it breaks guarantees that Immer otherwise keeps: shift, pop and splice return raw base objects, so editing a removed object changes the previous state; reordering can expose original objects the same way and leave revoked drafts in the result; and its forward patches can fail to replay. In the audit, 123 of 4,913 three-step operation sequences break with the plugin and none without it. The performance summary reports Immer with the plugin separately.

Mutative vs hand-written reducers

A hand-written reducer that copies only what it changes is faster than any library that drafts state, and it was faster than Mutative in 129 of the suite's 150 cases. Mutative was faster when copying wide objects, such as records with 1,000 keys or class instances with 100 fields, by 1.5-2.8x, when inserting at the front of 10,000 rows: 16 µs instead of 28 µs, and when adding a number to a Set of 1,000 or 10,000 numbers, which it copies faster than new Set() does: 30 µs instead of 61 µs at 10,000.

Spreads also become slow when code copies the same structure again and again. Mutative copies each object at most once per update, however many changes a recipe makes.

See Performance tips for recipes that avoid needless drafts, searches, copies, freezing and patches.

Run the benchmarks

From a clone of the repository:

pnpm install --frozen-lockfile
pnpm benchmark:immer:check
pnpm benchmark:immer

The benchmark guide describes the workloads, options, memory measurements, and CI regression budgets.

Edit on GitHub

Last updated on by unadlib

On this page