An artificial example where MobX really shines and Redux is not really suited for it

First, a word of caution. In this article, I am not saying that MobX is better, or that I prefer it, or anything.

I use both Redux and MobX.

I like both of them and probably will use both of them again.

This article only gives an artificial example of an application that is easy to write with MobX but very hard with Redux, if we need it to run at the same performance level. Had I not been clear, this is an unfair comparison.

Therefore this article is not a comprehensive comparison at all and should not be considered based on a real-world situation. So please don’t point to an artificial benchmark like this one and say “let’s all use this instead of that. It’s faster.

I don’t believe there’s a single best tool. I believe that each tool has its own area where it shines and areas where it’s not so good at.

There’s a time for everything. I do this kind of experiments to better understand the tools that I use, so that I can choose the right tool for the right job in the future.

With that said, let’s continue.

Versions

Just like other things in the JavaScript world, I believe this article will get-of-date pretty quickly as things gets more optimized. That’s why I’m going to list the version numbers here:

  • react@15.4.1, react-dom@15.4.1
  • redux@3.6.0, react-redux@5.0.0-rc.1
  • mobx@2.6.4, mobx-react@4.0.3

The task: Pixel paint

I’m going to create a “pixel paint” app using React. It features a canvas that displays drawable pixels on a 128×128 grid.

You can paint on any pixel by hovering it with your mouse.

We will render two canvases side-by-side, but both canvases share the same image. Otherwise, each pixel could’ve used its own local component state and we would not need to use any state-management library at all.

Each pixel is represented by a <div>.

So that’s 128×128×2=32768 DOM nodes to render and update.

This experiment is going to be very slow.

Note: All tests are done on production builds.

The MobX version

Here’s the store. (I’m avoiding the decorator syntax, because as of writing, the syntax is pending proposal.)

const store = observable({
pixels: asMap({ }),
isActive (i, j) {
return !!store.pixels.get(i + ',' + j)
},
toggle: action(function toggle (i, j) {
store.pixels.set(i + ',' + j, !store.isActive(i, j))
})
})

Our canvas renders each pixel.

function MobXCanvas () {
const items = [ ]
for (let i = 0; i < 128; i++) {
for (let j = 0; j < 128; j++) {
items.push(<PixelContainer i={i} j={j} key={i + ',' + j} />)
}
}
return <div>{items}</div>
}

Each Pixel observes its own piece of state in the store.

const PixelContainer = observer(function PixelContainer ({ i, j }) {
return <Pixel
i={i}
j={j}
active={store.isActive(i, j)}
onToggle={() => store.toggle(i, j)}
/>
})

Here’s the result. You can access the mobx-react-devtools at the top-right corner of the screen.

This is the result of profiling the app’s performance:

In the pie chart above, scripting takes only a small fraction. Most time is spent rendering and painting.

So that means MobX is doing its best job!

The Redux version: first attempt

Here’s the reducer:

const store = createStore((state = Immutable.Map(), action) => {
if (action.type === 'TOGGLE') {
const key = action.i + ',' + action.j
return state.set(key, !state.get(key))
}
return state
})

The selector:

const selectActive = (state, i, j) => state.get(i + ',' + j)

The action creator:

const toggle = (i, j) => ({ type: 'TOGGLE', i, j })

Our Redux store is ready.

Our canvas provides the store to each pixel:

function ReduxCanvas () {
const items = [ ]
for (let i = 0; i < 128; i++) {
for (let j = 0; j < 128; j++) {
items.push(<PixelContainer i={i} j={j} key={i + ',' + j} />)
}
}
return <Provider store={store}>
<div>
{items}
</div>
</Provider>
}

And each pixel connects to that store.

const PixelContainer = connect(
(state, ownProps) => ({
active: selectActive(state, ownProps.i, ownProps.j)
}),
(dispatch, ownProps) => ({
onToggle: () => dispatch(toggle(ownProps.i, ownProps.j))
})
)(Pixel)

Here’s the result. You can use redux-devtools-extension to inspect the store state, actions, and do some time traveling.

It seems that this version is much slower than MobX’s version. Let’s look at the profiler.

A significant amount of time is spent running JavaScript. Almost 50%. That’s not good. Why is it taking so long?

Let’s do some profiling:

It turns out to be how Redux’s subscription model works.

In the above example, each pixel is connected to the store, which means it subscribes to the store. When it changes, each subscriber decides if it’s interested in that change, and reacts accordingly.

That means when I change a single pixel, Redux notifies all 32,768 subscribers.

This is same as Angular 1’s dirty checking mechanism. And its advice also holds for Redux: Don’t render too many things on the screen.

With Redux, you can only subscribe to the store as a whole. You can’t subscribe to a subtree of your state, because that subtree is just a plain old JavaScript object which can’t be subscribed to.

With MobX, every piece of state is an observable on its own. Therefore, in the MobX version, each pixel subscribes to its own state subtree. That’s why it’s so fast from the first attempt.

2nd attempt: Single subscriber

So, too many subscribers can be a problem. So this time, I’ll make it so that there’s only one subscriber.

Here, we create a Canvas component which will subscribe to the store and render all the pixels.

function ReduxCanvas () {
return <Provider store={store}><Canvas /></Provider>
}
const Canvas = connect(
(state) => ({ state }),
(dispatch) => ({ onToggle: (i, j) => dispatch(toggle(i, j)) })
)(function Canvas ({ state, onToggle }) {
const items = [ ]
for (let i = 0; i < 128; i++) {
for (let j = 0; j < 128; j++) {
items.push(<PixelContainer
i={i}
j={j}
active={selectActive(state, i, j)}
onToggle={onToggle}
key={i + ',' + j}
/>)
}
}
return <div>{items}</div>
})

The PixelContainer would then pass the props received from the Canvas to its Pixel.

class PixelContainer extends React.PureComponent {
constructor (props) {
super(props)
this.handleToggle = this.handleToggle.bind(this)
}
handleToggle () {
this.props.onToggle(this.props.i, this.props.j)
}
render () {
return <Pixel
i={this.props.i}
j={this.props.j}
active={this.props.active}
onToggle={this.handleToggle}
/>
}
}

Here’s the result.

This version performs even worse that the first attempt.

Let’s see what’s going on.

The problem seems to be our Canvas. Since it’s the only one subscribing to the store, it’s now responsible for managing all the 16,384 pixels.

For each store dispatch, it needs to render 16,384 Pixels with the correct props.

This means 16,384 React.createElement calls followed by React trying to reconcile 16,384 children, for each canvas. Not so good.

We can do better!

3rd attempt: A balanced tree

One of Redux’s key strengths is in its immutable state tree (which enables cool features such as painless hot-reloading and time-traveling).

It turns out that the way we structure our data and our view is not so immutable-friendly.

An immutable state tree works best when it’s stored in a balanced tree. I discussed about this idea in this article:

So let’s do the same with our app.

We’ll subdivide our canvas into four quadrants.

When we need to change a pixel,

We can update just the relevant quadrant, leaving other 3 quadrants alone.

Instead of re-rendering all 16,384 pixels, we can re-render just 64×64=4096 pixels. This is 75% savings in performance.

4,096 is still a large number. So what we’ll do is we’ll subdivide our canvas recursively until we reach a 1×1 pixel canvas.

To be able to update the component this way, we need to structure our state in the same way too, so that when the state changes, we can use the === operator to determine if the quadrant’s state has been changed or not.

Here’s the code to (recursively) generate an initial state:

const generateInitialState = (size) => (size === 1
? false
: Immutable.List([
generateInitialState(size / 2),
generateInitialState(size / 2),
generateInitialState(size / 2),
generateInitialState(size / 2)
])
)

Now that our state is a recursively-nested tree, instead of referring to each pixel by its coordinate like (58, 52), we’re need to refer to each pixel by its path like (1, 3, 3, 2, 0, 2, 1) instead.

But to present them on the screen, we need to be able to figure out the coordinates from the path:

function keyPathToCoordinate (keyPath) {
let i = 0
let j = 0
for (const quadrant of keyPath) {
i <<= 1
j <<= 1
switch (quadrant) {
case 0: j |= 1; break
case 2: i |= 1; break
case 3: i |= 1; j |= 1; break
default:
}
}
return [ i, j ]
}

And we also need to do the inverse:

function coordinateToKeyPath (i, j) {
const keyPath = [ ]
for (let threshold = 64; threshold > 0; threshold >>= 1) {
keyPath.push(i < threshold
? j < threshold ? 1 : 0
: j < threshold ? 2 : 3
)
i %= threshold
j %= threshold
}
return keyPath
}

Now we can change our reducer to look like this:

const store = createStore(
function reducer (state = generateInitialState(128), action) {
if (action.type === 'TOGGLE') {
const keyPath = coordinateToKeyPath(action.i, action.j)
return state.updateIn(keyPath, (active) => !active)
// |
// This is why I use Immutable.js:
// So that I can use this method.
}
return state
}
)

Then we create a component to traverse this tree and put everything in place. The GridContainer connects to the store and renders the outermost Grid.

function ReduxCanvas () {
return <Provider store={store}><GridContainer /></Provider>
}
const GridContainer = connect(
(state, ownProps) => ({ state }),
(dispatch) => ({ onToggle: (i, j) => dispatch(toggle(i, j)) })
)(function GridContainer ({ state, onToggle }) {
return <Grid keyPath={[ ]} state={state} onToggle={onToggle} />
})

Then each Grid renders a smaller version of itself recursively until it reaches a leaf (a white/black 1x1 pixel canvas).

class Grid extends React.PureComponent {
constructor (props) {
super(props)
this.handleToggle = this.handleToggle.bind(this)
}
shouldComponentUpdate (nextProps) {
// Required since we construct a new `keyPath` every render
// but we know that each grid instance will be rendered with
// a constant `keyPath`. Otherwise we need to memoize the
// `keyPath` for each children we render to remove this
// "escape hatch."

return this.props.state !== nextProps.state
}
handleToggle () {
const [ i, j ] = keyPathToCoordinate(this.props.keyPath)
this.props.onToggle(i, j)
}
render () {
const { keyPath, state } = this.props
if (typeof state === 'boolean') {
const [ i, j ] = keyPathToCoordinate(keyPath)
return <Pixel
i={i}
j={j}
active={state}
onToggle={this.handleToggle}
/>
} else {
return <div>
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 0 ]} state={state.get(0)} />
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 1 ]} state={state.get(1)} />
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 2 ]} state={state.get(2)} />
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 3 ]} state={state.get(3)} />
</div>
}
}
}

Here’s the result.

Phew, we are back to speed! It feels as fast as the MobX version. Plus you can do hot-reloading and time-travel as well.

Our DOM tree also looks more tree-ish:

Compared to all previous approaches:

The ultimate optimization

I haven’t coded this one because it’s useless.

Here’s how: Create a Redux store for each pixel. I haven’t tested this but I’m quite sure this method is the fastest way that can be accomplished with Redux.

You also lose most benefits of using Redux if you really go down this path. For example, Redux DevTools probably breaks. And time traveling one pixel at a time isn’t so useful, isn’t it?

A better solution?

So that’s all I can think of.

If you know of a better/more elegant solution, please let me know.

Updates:

Conclusions

This was a fun experiment.

Many of us have a solid knowledge in optimizing imperative algorithms, but when it comes to apps based on immutable data, it can be challenging to optimize if we don’t know the performance implications.

Once we optimized the Redux version, we can see that performance optimizations can result in a less readable code. What a mess I did above!

As Dan Abramov said, Redux offers a trade-off (and so is MobX). So, will you trade the clarity and readability of your code for performance without losing the ability to hot-reload and do time-travel?

In my midi-instruments project, the app will be run in MobileSafari, so performance matters a lot, especially when an instrument may contain hundreds of buttons.

I also want to quickly prototype new instruments without having to worry about performance implications when I use immutable data.

I also find hot-reloading and time-traveling not-so-useful in this project. Most of the state lasts for a few seconds and my project is small enough that I can just reload the page.

So I happily use MobX in this project.

In a rhythm game that I’m building, Bemuse, I feel that using immutable data helps me write simple and easy-to-test code.

I don’t have to worry about unexpected state mutations because there is nothing to be mutated.

There’s not too much data to render either, so I probably don’t need to optimize it like the above example.

Having a Redux DevTools at hand and all state-updating centralized at a single place would also benefit me a lot. Here, Redux shines a lot!

So I happily use Redux in this project.

An unfair performance comparison

This comparison has been unfair from the beginning when I try to compare a functional approach (Redux) with an imperative approach (MobX).

In 1996, Chris Okasaki concluded in his 140-page thesis, ‘Purely Functional Data Structures’ that:

Regardless of the advances in compiler technology, functional programs will never be faster than their imperative counterparts as long as the algorithms available to functional programmers are significantly slower than those available to imperative programmers.

In that thesis (now available as a book), he tried to make data structures in functional programming as efficient its imperative counterpart.

This thesis provides numerous functional data structures that are asymptotically just as efficient as the best imperative implementations

I would not stop doing functional programming just because it can never be as fast as imperative algorithm…

It’s all about trade-offs. That’s why I never say ‘let’s use Redux/MobX for everything!.’ That’s why I can’t provide an adequate answer when people asked “should I use MobX or Redux in 2017?” without any other context. That’s why I wrote this article.

More by Thai Pangsakulyanont

Topics of interest

More Related Stories