Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Efficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer.

License

NotificationsYou must be signed in to change notification settings

unadlib/mutative

Repository files navigation

Mutative Logo

Node CICoverage Statusnpmlicense

Mutative - A JavaScript library for efficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer.

Why is Mutative faster than the spread operation(naive handcrafted reducer)?

The spread operation has performance pitfalls, which can be detailed in the following article:

And Mutative optimization focus on shallow copy optimization, more complete lazy drafts, finalization process optimization, and more.

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.

Mutative vs Naive Handcrafted Reducer Performance

Mutative vs Reducer benchmark by object:
  • Naive handcrafted reducer
// baseState type: Record<string, { value: number }>conststate={  ...baseState,key0:{    ...baseState.key0,value:i,},};
  • Mutative
conststate=create(baseState,(draft)=>{draft.key0.value=i;});

Mutative vs Reducer benchmark by object

Measure(seconds) to update the 1K-100K items object, lower is better(view source).

Mutative is up to 2x faster than naive handcrafted reducer for updating immutable objects.

Mutative vs Reducer benchmark by array:
  • Naive handcrafted reducer
// baseState type: { value: number }[]// slower 6x than Mutativeconststate=[{ ...baseState[0],value:i},  ...baseState.slice(1,baseState.length),];// slower 2.5x than Mutative// const state = baseState.map((item, index) =>//   index === 0 ? { ...item, value: i } : item// );// same performance as Mutative// const state = [...baseState];// state[0] = { ...baseState[0], value: i };

The actual difference depends on which spread operation syntax you use.

  • Mutative
conststate=create(baseState,(draft)=>{draft[0].value=i;});

Mutative vs Reducer benchmark by array

Measure(seconds) to update the 1K-100K items array, lower is better(view source).

Mutative is up to 6x faster than naive handcrafted reducer for updating immutable arrays.

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 v1.0.5 vs Immer v10.1.1]

Benchmark

Naive handcrafted reducer - No Freeze x 4,442 ops/sec ±0.38% (95 runs sampled)Mutative - No Freeze x 6,199 ops/sec ±0.79% (89 runs sampled)Immer - No Freeze x 5.30 ops/sec ±0.38% (18 runs sampled)Mutative - Freeze x 974 ops/sec ±1.77% (92 runs sampled)Immer - Freeze x 376 ops/sec ±0.67% (92 runs sampled)Mutative - Patches and No Freeze x 969 ops/sec ±0.99% (97 runs sampled)Immer - Patches and No Freeze x 5.27 ops/sec ±0.36% (18 runs sampled)Mutative - Patches and Freeze x 514 ops/sec ±0.97% (95 runs sampled)Immer - Patches and Freeze x 275 ops/sec ±0.74% (89 runs sampled)The fastest method is Mutative - No Freeze

Runyarn benchmark to measure performance.

OS: macOS 14.2.1, CPU: Apple M1 Max, Node.js: v20.11.0

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 16x performance gap between Mutative (6,199 ops/sec) and Immer (376 ops/sec).

Overall, Mutative has a huge performance lead over Immer inmore performance testing scenarios. Runyarn performance to get all the performance results locally.

More Performance Testing Scenarios, Mutative is up to `2.5X-73.8X` faster than Immer:

Mutative vs Immer - All benchmark results by average multiplier

view source.

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

MutativeImmer
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

yarn add mutative

NPM

npm install mutative

CDN

  • Unpkg:<script src="https://unpkg.com/mutative"></script>
  • JSDelivr:<script src="https://cdn.jsdelivr.net/npm/mutative"></script>

Usage

import{create}from'mutative';constbaseState={foo:'bar',list:[{text:'coding'}],};conststate=create(baseState,(draft)=>{draft.list.push({text:'learning'});});expect(state).not.toBe(baseState);expect(state.list).not.toBe(baseState.list);

create(baseState, (draft) => void, options?: Options): newState

The first argument ofcreate() is the base state. Mutative drafts it and passes it to the arguments of the draft function, and performs the draft mutation until the draft function finishes, then Mutative will finalize it and produce the new state.

Usecreate() for more advanced features bysettingoptions.

APIs

create()

Usecreate() for draft mutation to get a new state, which also supports currying.

import{create}from'mutative';constbaseState={foo:'bar',list:[{text:'todo'}],};conststate=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, andcreate() 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 usingunsafe()).

    When strict mode is enabled, mutable data can only be accessed usingunsafe().

    It is recommended to enablestrict 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 userawReturn().

  • 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 setpathAsArray andarrayLengthAssignmentpathAsArray 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 indevelopment mode.

  • mark -(target) => ('mutable'|'immutable'|function) | (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 andPatches should both be disabled, Some patches operation might not be equivalent).When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn onAutoFreeze andPatches.mark supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used.When a object tree node is marked by themark function asmutable, all of its child nodes will also not be drafted by Mutative and will retain their original values.

create() - Currying

  • createdraft
const[draft,finalize]=create(baseState);draft.foobar.bar='baz';conststate=finalize();

Support set options such asconst [draft, finalize] = create(baseState, { enableAutoFreeze: true });

  • createproducer
constproduce=create((draft)=>{draft.foobar.bar='baz';});conststate=produce(baseState);

Also support set options such asconst produce = create((draft) => {}, { enableAutoFreeze: true });

apply()

Useapply() for applying patches to get the new state.

import{create,apply}from'mutative';constbaseState={foo:'bar',list:[{text:'todo'}],};const[state,patches,inversePatches]=create(baseState,(draft)=>{draft.foo='foobar';draft.list.push({text:'learning'});},{enablePatches:true,});constnextState=apply(baseState,patches);expect(nextState).toEqual(state);constprevState=apply(state,inversePatches);expect(prevState).toEqual(baseState);

current()

Get the current value from a draft.

  • For any draft where a child node has been modified, the state obtained by executing current() each time will be a new reference object.
  • For a draft where no child nodes have been modified, executing current() will always return the original state.

It is recommended to minimize the number of times current() is executed when performing read-only operations, ideally executing it only once.

conststate=create({a:{b:{c:1}},d:{f:1}},(draft)=>{draft.a.b.c=2;expect(current(draft.a)).toEqual({b:{c:2}});// The node `a` has been modified.expect(current(draft.a)===current(draft.a)).toBeFalsy();// The node `d` has not been modified.expect(current(draft.d)===current(draft.d)).toBeTruthy();});

original()

Get the original value from a draft.

constbaseState={foo:'bar',list:[{text:'todo'}],};conststate=create(baseState,(draft)=>{draft.foo='foobar';draft.list.push({text:'learning'});expect(original(draft.list)).toEqual([{text:'todo'}]);});

unsafe()

When strict mode is enabled, mutable data can only be accessed usingunsafe().

constbaseState={list:[],date:newDate(),};conststate=create(baseState,(draft)=>{unsafe(()=>{draft.date.setFullYear(2000);});// or return the mutable data:// const date = unsafe(() => draft.date);},{strict:true,});

isDraft()

Check if a value is a draft.

constbaseState={date:newDate(),list:[{text:'todo'}],};conststate=create(baseState,(draft)=>{expect(isDraft(draft.date)).toBeFalsy();expect(isDraft(draft.list)).toBeTruthy();});

isDraftable()

Check if a value is draftable

constbaseState={date:newDate(),list:[{text:'todo'}],};expect(isDraftable(baseState.date)).toBeFalsy();expect(isDraftable(baseState.list)).toBeTruthy();

You can set a mark to determine if the value is draftable, and the mark function should be the same as passing increate() mark option.

rawReturn()

For return values that do not contain any drafts, you can userawReturn() to wrap this return value to improve performance. It ensure that the return value is only returned explicitly.

constbaseState={id:'test'};conststate=create(baseStateas{id:string}|undefined,(draft)=>{returnrawReturn(undefined);});expect(state).toBe(undefined);

If the return value mixes drafts, you should not userawReturn().

constbaseState={a:1,b:{c:1}};conststate=create(baseState,(draft)=>{if(draft.b.c===1){return{      ...draft,a:2,};}});expect(state).toEqual({a:2,b:{c:1}});expect(isDraft(state.b)).toBeFalsy();

If you userawReturn(), we recommend that you enablestrict mode in development.

constbaseState={a:1,b:{c:1}};conststate=create(baseState,(draft)=>{if(draft.b.c===1){returnrawReturn({        ...draft,a:2,});}},{strict:true,});// it will warn `The return value contains drafts, please don't use 'rawReturn()' to wrap the return value.` in strict mode.expect(state).toEqual({a:2,b:{c:1}});expect(isDraft(state.b)).toBeFalsy();

makeCreator()

makeCreator() only takesoptions as the first argument, resulting in a customcreate() function.

constbaseState={foo:{bar:'str',},};constcreate=makeCreator({enablePatches:true,});const[state,patches,inversePatches]=create(baseState,(draft)=>{draft.foo.bar='new str';});

markSimpleObject()

markSimpleObject() is a mark function that marks all objects as immutable.

constbaseState={foo:{bar:'str',},simpleObject:Object.create(null),};conststate=create(baseState,(draft)=>{draft.foo.bar='new str';draft.simpleObject.a='a';},{mark:markSimpleObject,});expect(state.simpleObject).not.toBe(baseState.simpleObject);

View more API docs.

Using TypeScript

  • castDraft()
  • castImmutable()
  • Draft<T>
  • Immutable<T>
  • Patches
  • Patch
  • Options<O, F>

Integration with React

  • use-mutative - A 2-6x faster alternative to useState with spread operation
  • use-travel - A React hook for state time travel with undo, redo, reset and archive functionalities.

FAQs

  • 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. React Native uses a new JS engine during refactoring - Hermes, and it (if < v0.59 or when using the Hermes engine on React Native < v0.64) doesnot support Proxy on Android, butReact Native v0.64 with the Hermes engine support Proxy.

  • Can Mutative be integrated with Redux?

Yes. Mutative supports return values for reducer, andredux-toolkit is considering support forconfigurableproduce().

Migration from Immer to Mutative

mutative-compat - Mutative wrapper with full Immer API compatibility, you can use it to quickly migrate from Immer to Mutative.

  1. produce() ->create()

Mutative auto freezing option is disabled by default, Immer auto freezing option is enabled by default (if disabled, Immer performance will have a more huge drop).

You need to check if auto freezing has any impact on your project. If it depends on auto freezing, you can enable it yourself in Mutative.

importproducefrom'immer';constnextState=produce(baseState,(draft)=>{draft[1].done=true;draft.push({title:'something'});});

Use Mutative

import{create}from'mutative';constnextState=create(baseState,(draft)=>{draft[1].done=true;draft.push({title:'something'});});
  1. Patches
import{produceWithPatches,applyPatches}from'immer';enablePatches();constbaseState={age:33,};const[nextState,patches,inversePatches]=produceWithPatches(baseState,(draft)=>{draft.age++;});conststate=applyPatches(nextState,inversePatches);expect(state).toEqual(baseState);

Use Mutative

import{create,apply}from'mutative';constbaseState={age:33,};const[nextState,patches,inversePatches]=create(baseState,(draft)=>{draft.age++;},{enablePatches:true,});conststate=apply(nextState,inversePatches);expect(state).toEqual(baseState);
  1. Returnundefined
importproduce,{nothing}from'immer';constnextState=produce(baseState,(draft)=>{returnnothing;});

Use Mutative

import{create,rawReturn}from'mutative';constnextState=create(baseState,(draft)=>{returnrawReturn(undefined);});

Contributing

Mutative goal is to provide efficient and immutable updates. The focus is on performance improvements and providing better APIs for better development experiences. We are still working on it and welcome PRs that may help Mutative.

Development Workflow:

  • Clone Mutative repo.
  • Runyarn install to install all the dependencies.
  • Runyarn prettier to format the code.
  • yarn test --watch runs an interactive test watcher.
  • Runyarn commit to make a git commit.

License

Mutative isMIT licensed.

About

Efficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp