32

I have a series of mutations to make on my Immutable.js map.

At what point should I prefer using withMutations rather than creating intermediate immutable maps?

From the Immutable.js docs:

If you need to apply a series of mutations to produce a new immutable Map, withMutations() creates a temporary mutable copy of the Map which can apply mutations in a highly performant manner. In fact, this is exactly how complex mutations like merge are done.

But, where is the line? If I need to make two mutations should I use withMutations?

dstreit
  • 716
  • 1
  • 7
  • 8

3 Answers3

32

You should use withMutations when you want to group several changes on an object.

Because each change creates a clone, several changes in a row cause several clones. Use withMutations to apply several changes then clone once at the end. A clone at every step means that you are reading the entire list for each change, which makes it an n^2 operation. Using the mutatable version leaves it at just N.

This can create a massive performance hit if you just start doing things willy nilly like the other answer suggests. The kicker is it might not matter on a small test of data, but what happens if you push it live and then do a large scale operation on an array with 300,000 pieces of data instead of your test data which only has 100 elements. In your test data, you did 10,000 copies of which you might not notice. In production you would do 900,000,000,00 copies which might light your computer on fire (not really, but you would freeze the tab).

I wrote a demo demonstrating the performance difference http://jsperf.com/with-out-mutatable

Eric Wooley
  • 646
  • 4
  • 17
  • 2
    Your performance test is misguided: you are doing 10 000 mutations to one list. If you need to do that, then definitely use withMutations. A more realistic scenario would be to do 5 mutations to a list with 10 000 items. – OlliM May 21 '15 at 14:19
  • I'm also surprised no one called me out on this, but it wouldn't necessarily be an n^2 op, it could be n*(mutation ops), which could be much smaller. Depends on what your doing. – Eric Wooley Jun 22 '17 at 16:10
  • 3
    This answer is somewhat incorrect. ImmutableJS does not clone the entire data structure every time you modify it. It does create a new object, but it does not copy over all the data, so it can achieve better than `O(n^2)` for n operations. For example `list.push()` takes constant time, so calling `list = list.push(x)` n times will take `O(n)` time. https://facebook.github.io/immutable-js/docs/#/List You can still get some extra performance out of `withMutations`, but you probably aren't decreasing the algorithmic complexity. – Simon Baumgardt-Wellander Oct 28 '17 at 01:22
  • @Simon You are correct, though I don't remember if the data structures were the same at the time of writing this answer. – Eric Wooley Oct 28 '17 at 22:34
13

I've also been trying to decide when it's best to use withMutations vs not. So far I have always been using withMutations when I need to loop through a list of items with unknown size n, and perform n mutations on a single map. Example:

updateItems(state, items) {
    return state.withMutations(state => {
        items.forEach(item => {
            state.set(item.get('id'), item);
         });
    });
}

What I wanted to know, is if I should use withMutations on a smaller known set of updates, or if I should just chain set.

Example:

setItems(state, items, paging) {
    return state.set('items', items).set('paging', paging);
}

Or:

setItems(state, items, paging) {
    return state.withMutations(state => {
        state.set('items', items);
        state.set('paging', paging);
    });
}

In this case, I would prefer to use the first option (chained set calls), but I wasn't sure of the performance impact. I created this jsPerf to test some similar cases: http://jsperf.com/immutable-batch-updates-with-mutations.

I ran batches of 2 updates and 100 updates, using an empty inital map, and a complex initial map, both using withMutations and just chaining set.

The results seem to show that withMutations was always better on the 100 update case, regardless of initial map. withMutations performed marginally better on the 2 update case when starting with an empty map. However, just chaining 2 set calls performed better when starting with a complex map.

Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
Wade Peterson
  • 131
  • 1
  • 2
  • 2
    I might be coming to this party too late, but chaining the `set` method as you do in the first case is creating a new copy after each method call, while in the second case, it's cloned just once. – kudlajz Nov 20 '16 at 15:01
1

I would just create intermediate immutable maps and then use some profiling tools to check if you actually have any performance problems with that approach. No need to do performance optimisations before you have performance problems.

As a rule of thumb I prefer to use existing functionality like .map, .filter etc. without any thought of withMutations. If there is a need to create a new List (or Map) where each element is mutated, I would first question that need and try to solve the problem using existing tools and functional programming style. If that is impossible, I would create a custom mutation with withMutations.

OlliM
  • 7,023
  • 1
  • 36
  • 47