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:
100 | 1000 | 10k | 100k | 1M | 10M | |
---|---|---|---|---|---|---|
Immer | 11 ms | 94 ms | 8.8 sec | 17 min 47 sec | ??? | ??? |
Immutable.JS | 3 ms | 9 ms | 47 ms | 257 ms | 2.4 s | 41 s |
Builtin Map | 37 µs | 180 µs | 2 ms | 18 ms | 170 ms | 3.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.