Mutative - 10x faster than Immer
2022-12-31
Mutative - A JavaScript library for efficient immutable updates, faster than naive handcrafted reducer, and more than 10x faster than Immer.
Motivation
Writing immutable updates by hand is usually difficult, prone to errors, and cumbersome. Immer helps us write simpler immutable updates with “mutative” logic.
But its performance issue causes a runtime performance overhead. Immer must have auto-freeze enabled by default(Performance will be worse if auto-freeze is disabled), such immutable state with Immer is not common. In scenarios such as cross-processing, remote data transfer, etc., we have to constantly freeze these immutable data.
There are more parts that could be improved, such as better type inference, non-intrusive markup, support for more types of immutability, Safer immutability, more edge cases, and so on.
This is why Mutative was created.
Repo: https://github.com/unadlib/mutative
Mutative vs Immer Performance
Mutative passed all of Immer’s test cases.
Measure(ops/sec) to update 50K arrays and 1K objects, bigger is better(view source). [Mutative v0.5.0 vs Immer v10.0.1]
Naive handcrafted reducer - No Freeze x 3,692 ops/sec ±1.28% (95 runs sampled)
Mutative - No Freeze x 5,425 ops/sec ±1.70% (93 runs sampled)
Immer - No Freeze x 5.08 ops/sec ±0.58% (17 runs sampled)
Mutative - Freeze x 818 ops/sec ±1.49% (96 runs sampled)
Immer - Freeze x 357 ops/sec ±0.79% (92 runs sampled)
Mutative - Patches and No Freeze x 746 ops/sec ±0.83% (96 runs sampled)
Immer - Patches and No Freeze x 5.08 ops/sec ±0.25% (17 runs sampled)
Mutative - Patches and Freeze x 408 ops/sec ±0.28% (96 runs sampled)
Immer - Patches and Freeze x 264 ops/sec ±0.59% (91 runs sampled)
The fastest method is Mutative - No Freeze
Run yarn benchmark
to measure performance.
OS: macOS 12.6, CPU: Apple M1 Max, Node.js: 16.14.2
Immer relies on auto-freeze to be enabled, if auto-freeze is disabled, Immer will have a huge performance drop and Mutative will have a huge performance lead, especially with large data structures it will have a performance lead of more than 50x.
So if you are using Immer, you will have to enable auto-freeze for performance. Mutative is disabled auto-freeze by default. With the default configuration of both, we can see the 17x performance gap between Mutative (6,421 ops/sec
) and Immer (365 ops/sec
).
Overall, Mutative has a huge performance lead over Immer in more performance testing scenarios. Run yarn performance
to get all the performance results locally.
OS: macOS 12.6, CPU: Apple M1 Max, Node.js: 16.14.2
Immer relies on auto-freeze to be enabled, if auto-freeze is disabled, Immer will have a huge performance drop and Mutative will have a huge performance lead, especially with large data structures it will have a performance lead of more than 50x.
So if you are using Immer, you will have to enable auto-freeze for performance. Mutative is disabled auto-freeze by default. With the default configuration of both, we can see the performance gap between Mutative (5,323 ops/sec
) and Immer (320 ops/sec
).
Overall, Mutative has a huge performance lead over Immer in more performance testing scenarios. Run yarn performance
to get all the performance results locally.
Features and Benefits
- Mutation makes immutable updates - Immutable data structures supporting objects, arrays, Sets and Maps.
- High performance - 10x faster than immer by default, even faster than naive handcrafted reducer.
- Optional freezing state - No freezing of immutable data by default.
- Support for JSON Patch - Full compliance with JSON Patch specification.
- Custom shallow copy - Support for more types of immutable data.
- Support mark for immutable and mutable data - Allows for non-invasive marking.
- Safer mutable data access in strict mode - It brings more secure immutable updates.
- Support for reducer - Support reducer function and any other immutable state library.
Difference between Mutative and Immer
Mutative | Immer | |
---|---|---|
Custom shallow copy | ✅ | ❌ |
Strict mode | ✅ | ❌ |
No data freeze by default | ✅ | ❌ |
Non-invasive marking | ✅ | ❌ |
Complete freeze data | ✅ | ❌ |
Non-global config | ✅ | ❌ |
async draft function | ✅ | ❌ |
Fully compatible with JSON Patch spec | ✅ | ❌ |
Mutative has fewer bugs such as accidental draft escapes than Immer, view details.
Installation
yarn install mutative # npm install mutative
Usage
Use create()
for draft mutation to get a new state, which also supports currying.
import { create } from 'mutative';
const baseState = {
foo: 'bar',
list: [{ text: 'todo' }],
};
const state = create(baseState, (draft) => {
draft.foo = 'foobar';
draft.list.push({ text: 'learning' });
});
In this basic example, the changes to the draft are ‘mutative’ within the draft callback, and create()
is finally executed with a new immutable state.
create(state, fn, options)
- Then options is optional.
strict -
boolean
, the default is false.Forbid accessing non-draftable values in strict mode(unless using unsafe()).
It is recommended to enable
strict
in development mode and disablestrict
in production mode. This will ensure safe explicit returns and also keep good performance in the production build. If the value that does not mix any current draft or isundefined
is returned, then use rawReturn().enablePatches -
boolean | { pathAsArray?: boolean; arrayLengthAssignment?: boolean; }
, the default is false.Enable patch, and return the patches/inversePatches.
If you need to set the shape of the generated patch in more detail, then you can set
pathAsArray
andarrayLengthAssignment
。pathAsArray
default value istrue
, if it’strue
, the path will be an array, otherwise it is a string;arrayLengthAssignment
default value istrue
, if it’strue
, the array length will be included in the patches, otherwise no include array length(NOTE: IfarrayLengthAssignment
isfalse
, it is fully compatible with JSON Patch spec, but it may have additional performance loss), view related discussions.enableAutoFreeze -
boolean
, the default is false.Enable autoFreeze, and return frozen state, and enable circular reference checking only in
development
mode.mark -
(target) => ('mutable'|'immutable'|function)
Set a mark to determine if the value is mutable or if an instance is an immutable, and it can also return a shallow copy function(AutoFreeze and Patches should both be disabled).
FAQs
- Why does Mutative have such good performance?
Mutative optimization focus is on shallow copy optimization, more complete lazy drafts, finalization process optimization, and more.
- I’m already using Immer, can I migrate smoothly to Mutative?
Yes. Unless you have to be compatible with Internet Explorer, Mutative supports almost all of Immer features, and you can easily migrate from Immer to Mutative.
Migration is also not possible for React Native that does not support Proxy.
Conclusion
Mutative is inspired by Immer.
Mutative aims at efficient immutable updates, focusing on performance improvements and better APIs to bring better development experience. If you think Mutative is good, feel free to give it a star!