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:
[email protected]
,[email protected]
[email protected]
,[email protected]
[email protected]
,[email protected]
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 <Pixeli={i}j={j}active={store.isActive(i, j)}onToggle={() => store.toggle(i, j)}/>})
Hereās the result. You can access the [mobx-react-devtools](https://github.com/mobxjs/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.jreturn 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 connect
s 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 connect
ed 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(<PixelContaineri={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 <Pixeli={this.props.i}j={this.props.j}active={this.props.active}onToggle={this.handleToggle}/>}}
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 **Pixel**
s 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:
Immutable.js, persistent data structures and structural sharing_Why use Immutable.js instead of normal JavaScript object?_medium.com
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 = 0let j = 0for (const quadrant of keyPath) {i <<= 1j <<= 1switch (quadrant) {case 0: j |= 1; breakcase 2: i |= 1; breakcase 3: i |= 1; j |= 1; breakdefault:}}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 %= thresholdj %= 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.propsif (typeof state === 'boolean') {const [ i, j ] = keyPathToCoordinate(keyPath)return <Pixeli={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>}}}
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:
- Dan Abramov submitted his version, which is faster than V1 but āless efficient than V3 but also way simpler.ā
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.
Thanks forĀ reading!
Thereās some good discussion on Reddit!
Thereās some more discussion in Dan Abramovās pull request (thanks!):
Add a simple but reasonably fast Redux version by gaearon Ā· Pull Request #1 Ā· dtinth/pixelpaint_It's less efficient than V3 but also way simpler. Relies on memoization of props (which is something you want to do inā¦_github.com