Optimizing React with Immutable.js

The web gets faster everyday. This is just as much of a good thing for the people working on the internet as it is for everyone else. A lot of this is due to the fact that our tools are improving and we're writing more performant programs. React, Elm, Cycle.js and many other frameworks have made abstractions over the DOM to improve performance and Web Sockets make real time communication very accessible.

So why would we care about optimizing code when everything is already so awesome?

You may not have to. If an action takes less than 100ms to respond to a user, you can count on that action feeling instant. From there, load time perception increases incrementally until around 10s when most users drop off completely.

Knowing this is really important. We can think about this in the planning stages of projects in order to determine if the extra time and resources to achieve better performance is the right trade off to make in your situation.

In a lot of situations. Optimizing your front end code won't be a factor and there are a lot of things you can do to improve your user's perception of performance. With that said, solving problems at scale requires attention to detail. When you're dealing with a large data set, updating your UI becomes expensive and can make your app seem sluggish. So if we decide that it's time to refactor and look for performance gains, we can find the answers by looking at the data.

class App extends React.Component {  
  constructor(props) {
    super(props)
  }

  render() {
    return(
      <div>
      {
        this.props.list.map((item, i) => {
          return <p key={item+i}>{item}</p>
        })
      }
      </div>
    )
  }
}

This React component has one job, to render a list. There's no reason why we would ever need to optimize a component like this, but a simple example is always best. React components follow a top-down architecture. This means that data flows into your application from a top level component and gets passed down to it's children. The reason this in important is because this setup gives us a guarantee that can help us optimize.

If your list prop has not changed, you do not need to re-render your list.

We can follow this rule to avoid rebuilding the DOM when this.props.list has not changed from the last time the DOM rendered. This should be simple enough, we can hook into React's lifecycle and check for a change in our input.

class App extends React.Component {  
  constructor(props) {
    super(props)
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextProps.list !== this.props.list)
    return (nextProps.list !== this.props.list)
  }

  render() {
    return(
      <div>
      {
        this.props.list.map((item) => {
          return <p key={item}>{item}</p>
        })
      }
      </div>
    )
  }
}

This should work fine for now. We check to see if the list has changed, and when it has we re-render the DOM.

Now, let's make a wrapper component that provides the list prop.

class AppWrapper extends React.Component {  
  constructor(props) {
    super(props)
    this.state = {
      list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
      otherList: [1, 2, 3, 4, 5]
    }
    this.updateList = this.updateList.bind(this)
    this.updateOtherList = this.updateOtherList.bind(this)
  }

  updateList() {
    this.setState({
      list: [...this.state.list.slice(0, 9), 11]
    })
  }

  updateOtherList() {
    this.setState({
      otherList: [...this.state.otherList.slice(0, 4), 6]
    })
  }

  render() {
    return(
      <div>
        <button onClick={ this.updateList }> 
          Update List  
        </button>  
        <button onClick={ this.updateOtherList }> 
          Update Other List  
        </button> 
        <App list={ this.state.list } />
      </div>
    )
  }
}

Our wrapper can test our App component by updating the list we pass in to it and also updating a separate list kept in the state. When you run this code, you can test our shouldComponentUpdate function in App by updating the list that is not getting passed in and the updating the list that is, and watch the console for our logs.

If you did that, you noticed that something went wrong. Our component updates with the new list but when we compare the list a second time, our App component still re-renders the list. This happens because we checked for referential equality on the array. So basically unless we are actually pointing to the same array, our equality check will come back true when we are checking for changes within the array.

2 arrays that hold identical values are not equal by reference

Because our app replaces the state with a new state, this approach will not work unless we iterate over the array and check each individual value. This is not only error prone but can become expensive quickly.

Immutable.js is a tool that tackles this problem. It was developed by Lee Byron & Facebook team and fits in nicely in the one way data flow that React promotes. This allows us to use referential equality as a way to know if our data has changed.

Under the hood, Immutable uses structural sharing as a way to do this efficiently. In short, if we have this set-

const firstArr = [1, 2, 3]  

and we update it to become this set

const secondArr = [1, 2, 3, 4]  

They can share the 1, 2, 3 and the second one will get 4.

This makes Immutable structures cheaper to create, which is nice since you'll do that every time you need to make an update.

Let's add Immutable to the project and try this out.

After importing the library, all we need to do is change our arrays into Immutable lists and then update those lists using List.set. So our new AppWrapper looks like this.

class AppWrapper extends React.Component {  
  constructor(props) {
    super(props)
    this.state = {
      list: Immutable.List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
      otherList: Immutable.List.of(1, 2, 3, 4, 5)
    }
    this.updateList = this.updateList.bind(this)
    this.updateOtherList = this.updateOtherList.bind(this)
  }

  updateList() {
    this.setState({
      list: this.state.list.set(9, 11)
    })
  }

  updateOtherList() {
    this.setState({
      otherList: this.state.otherList.set(4, 6)
    })
  }

  render() {
    return(
      <div>
        <button onClick={ this.updateList }> 
          Update List  
        </button>  
        <button onClick={ this.updateOtherList }> 
          Update Other List  
        </button> 
        <App list={ this.state.list } />
      </div>
    )
  }
}

If you run this code, you'll see the logs telling us that after you update the list once and we then try to update it again with the same values, we won't actually cause the App Component to re-render that second time around.

You can use this pattern on large data sets where it would actually give you a notable performance boost and if you enforce a top-to-bottom data flow you can count on all of your data having to pass equality checks.

Happy Friday!