Immutable.js vs Immer

Immutable.js vs Immer

This week I came across the library Immer as a convenient way of manipulating immutable data in JavaScript. After reading this Reddit thread that raves about how much better Immer is than Immutable.Js, I was worried I’d made the wrong decision to use Immutable.js in Microvium. But some performance tests quickly cleared up my concerns.

Immer certainly seems convenient to use. It uses a combination of proxies and copy-on-write to allow you to use the normal mutating syntax in JavaScript and it handles the underlying immutability automatically.

But “copy-on-write” raised a red flag in my mind. When dealing with small changes to large collections, as Microvium does, I can’t possibly imagine how copying the whole collection on every write can be efficient. And indeed, a quick performance test shows how bad the situation is:

import { produce } from "immer";
import immutable from 'immutable';

const count = 100;

// Test Immutable.JS
let map1 = immutable.Map();
for (let i = 0; i < count; i++)
  map1 = map1.set(i, i);

// Test Immer
let map2 = new Map();
for (let i = 0; i < count; i++)
  map2 = produce(map2, m => m.set(i, i));

For different values of count, here are my results:

100100010k100k1M10M
Immer11 ms94 ms8.8 sec17 min 47 sec??????
Immutable.JS3 ms9 ms47 ms257 ms2.4 s41 s
Builtin Map37 µs180 µs2 ms18 ms170 ms3.4 s

Insertion into the map using Immer is unsurprisingly O(n) relative to the size of the map, making the whole test O(n²). Even though I had a hunch that this was going to be the case, it was worth checking that Immer wasn’t doing some other clever tricks under the hood to improve the performance.

I’ve never actually tested the performance of Immutable.JS before, so I’m quite pleased to see how well it scales. It seems only slightly worse than O(1) insertion time.

Both libraries are quite a bit slower than using the built-in Map class, so my conclusions are as follows:

  • Use Immer if readability and ease-of-use is more important than performance, which is often the case. But be aware that it’s not a simple process to switch to Immutable.JS later because of how different the API is.
  • Use Immutable.JS if you need high-performance persistent collections that scale well to large sizes, if it’s worth the decrease in readability, type safety, and increased verbosity of the code.
  • Use the mutable built-in collections for performance-critical code that doesn’t strictly need immutability for the algorithm.

TC39 is looking to add built-in immutable collections to the JavaScript standard (see the records and tuples proposal). I’m excited to see how those perform.

2 Replies to “Immutable.js vs Immer”

  1. This is older one but I just want to say that you added O(n²) complexity yourself because you didn’t used correct immer pattern. You should have wrote immer test like this:

    let map2 = new Map();

    map2 = produce(map2, m => {

    for (let i = 0; i < count; i++)

    m.set(i,i);

    });

    This one is even faster than immutable.

    1. Yes, that’s a good point for readers to note. If your use case doesn’t require immutability on the intermediate changes then you can make all the changes at once as a batch, imperatively. This is similar in spirit to immutable.js’s withMutations function.

      But it does depend on what your use case is. The concern I raised with immer’s performance is when dealing with “small changes to large collections”. What I meant by that is, if each persisted change (transaction) is small, but you have lots of changes, then your performance may suffer. If you don’t need persistence on each small change then you can aggregate them into one large transaction as you’ve done.

      In my specific case, I have many, small transactions, so immutable.js is more efficient for my use case.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.