Search code examples
javascriptperformanceredux

Which is faster for a redux reducer: switch or map?


The redux documentation, when recommending best practices for reducer, mentions refactoring away switch statements and replacing them with a dictionary/map of action to handler, so instead of:

switch(action) {
  case 'action1':
   doAction1(payload);
  case 'action2':
   doActions2(payload);
}

you'd have something like:

var handlers = {
  'action1': doAction1,
  'action2': doAction2,
}
handlers[action](payload);

(see https://redux.js.org/usage/structuring-reducers/refactoring-reducer-example)

I can see that the dictionary approach is cleaner to read. I am wondering if this is the only reason it is preferred? Or does the dictionary outperform the switch too?


Solution

  • Some people have ideological objections to switch statements, partly because of the possibility of missing break statements accidentally and having fall-through cases. In the case of a redux reducer, that's not an issue since each case returns the new state, so I personally have no problem with the switch here, and I'm not even sure I agree the dictionary is any 'cleaner'.

    WRT performance, I would go with the stylistically preferred option because in 99.9% of redux cases, you won't be updating state frequently enough for such minor differences to matter. But OK, let's suck it and see which performs faster:

    const keys = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
    let count = 0;
    const increment = () => ++count; // just do something on each action
    console.time("switch");
    for (let i = 0; i < 10000000; i++) {
      const k = keys[Math.floor(Math.random() * keys.length)];
      switch (k) {
        case 'a':
          increment();
          break;
        case 'b':
          increment();
          break;
        case 'c':
          increment();
          break;
        case 'd':
          increment();
          break;
        case 'e':
          increment();
          break;
        case 'f':
          increment();
          break;
        case 'g':
          increment();
          break;
        case 'h':
          increment();
          break;
        case 'i':
          increment();
          break;
        case 'j':
          increment();
          break;
        case 'k':
          increment();
          break;
        case 'l':
          increment();
          break;
        case 'm':
          increment();
          break;
        case 'n':
          increment();
          break;
        case 'o':
          increment();
          break;
        case 'p':
          increment();
          break;
        case 'q':
          increment();
          break;
        case 'r':
          increment();
          break;
        case 's':
          increment();
          break;
        case 't':
          increment();
          break;
        case 'u':
          increment();
          break;
        case 'v':
          increment();
          break;
        case 'w':
          increment();
          break;
        case 'x':
          increment();
          break;
        case 'y':
          increment();
          break;
        case 'z':
          increment();
          break;
    
      }
    }
    console.timeEnd('switch');
    console.time('map');
    const map = keys.reduce((a, b) => {
      a[b] = increment;
      return a;
    }, {});
    
    for (let i = 0; i < 10000000; i++) {
      const k = keys[Math.floor(Math.random() * keys.length)];
      map[k]();
    }
    console.timeEnd('map');
    
    // switch: 529.752ms
    // map: 737.213ms
    

    So switch wins, by a significant, though not enormous margin. However, you'll never notice it in practice, so go with the one you prefer from a readability perspective.