React performance: rendering big list with PureRenderMixin

12k views Asked by At

I took a TodoList example to reflect my problem but obviously my real-world code is more complex.

I have some pseudo-code like this.

var Todo = React.createClass({
  mixins: [PureRenderMixin], 
  ............ 
}

var TodosContainer = React.createClass({
  mixins: [PureRenderMixin],    

  renderTodo: function(todo) {
     return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
  },

  render: function() {
     var todos = this.props.todos.map(this.renderTodo)
     return (
          <ReactCSSTransitionGroup transitionName="transition-todo">
                 {todos}
          </ReactCSSTransitionGroup>,
     );
  }

});

All my data is immutable, and PureRenderMixin is used appropriately and everything works fine. When a Todo data is modified, only the parent and the edited todo is re-rendered.

The problem is that at some point my list grows big as the user is scrolling. And when a single Todo is updated, it takes more and more time to render the parent, call shouldComponentUpdate on all the todos, and then render the single todo.

As you can see, the Todo component has other component than the Todo data. This is data that is required for render by all the todos and is shared (for example we could imagine there's a "displayMode" for the todos). Having many properties makes the shouldComponentUpdate perform a little bit slower.

Also, using ReactCSSTransitionGroup seems to slow down a little too, as ReactCSSTransitionGroup have to render itself and ReactCSSTransitionGroupChild even before the shouldComponentUpdate of todos is called. React.addons.Perf shows that ReactCSSTransitionGroup > ReactCSSTransitionGroupChild rendering is time wasted for each item of the list.

So, as far as I know, I use PureRenderMixin but with a larger list this may be not enough. I still get not so bad performances, but would like to know if there are easy ways to optimize my renderings.

Any idea?


Edit:

So far, my big list is paginated, so instead of having a big list of items, I now split this big list in a list of pages. This permits to have better performances as each page can now implement shouldComponentUpdate. Now when an item changes in a page, React only has to call the main render function that iterates on the page, and only call the render function from a single page, which make a lot less iteration work.

However, my render performance is still linear to the page number (O(n)) I have. So if I have thousands of pages it's still the same issue :) In my usecase it's unlikely to happen but I'm still interested in a better solution.

I am pretty sure it is possible to achieve O(log(n)) rendering performance where n is the number of items (or pages), by splitting a large list into a tree (like a persistent data structure), and where each node has the power to short-circuit the computation with shouldComponentUpdate

Yes I'm thinking of something akin to persistent data structures like Vector in Scala or Clojure:

http://hypirion.com/imgs/pvec/9-annotated.png

However I'm concerned about React because as far as I know it may have to create intermediate dom nodes when rendering the internal nodes of the tree. This may be a problem according to the usecase (and may be solved in future versions of React)

Also as we are using Javascript I wonder if Immutable-JS support this, and make the "internal nodes" accessible. See: https://github.com/facebook/immutable-js/issues/541

Edit: useful link with my experiments: Can a React-Redux app really scale as well as, say Backbone? Even with reselect. On mobile

3

There are 3 answers

0
Sebastien Lorber On BEST ANSWER

Here is a POC implementation I've done with ImmutableJS internal structure. This is not a public API so it is not ready for production and does not currently handle corner cases but it works.

var ImmutableListRenderer = React.createClass({
  render: function() {
    // Should not require to use wrapper <span> here but impossible for now
    return (<span>
        {this.props.list._root ? <GnRenderer gn={this.props.list._root}/> : undefined}
        {this.props.list._tail ? <GnRenderer gn={this.props.list._tail}/> : undefined}
</span>);
  }   
})

// "Gn" is the equivalent of the "internal node" of the persistent data structure schema of the question
var GnRenderer = React.createClass({
    shouldComponentUpdate: function(nextProps) {
      console.debug("should update?",(nextProps.gn !== this.props.gn));
      return (nextProps.gn !== this.props.gn);
    },
    propTypes: {
        gn: React.PropTypes.object.isRequired,
    },
    render: function() {
        // Should not require to use wrapper <span> here but impossible for now
        return (
            <span>
                {this.props.gn.array.map(function(gnItem,index) { 
                    // TODO should check for Gn instead, because list items can be objects too...
                    var isGn = typeof gnItem === "object"
                    if ( isGn ) {
                        return <GnRenderer gn={gnItem}/>
                    } else {
                        // TODO should be able to customize the item rendering from outside
                        return <span>{" -> " + gnItem}</span>
                    }
                }.bind(this))}
            </span>
        );
    }
})

The client code looks like

React.render(
    <ImmutableListRenderer list={ImmutableList}/>, 
    document.getElementById('container')
);

Here is a JsFiddle that logs the number of shouldComponentUpdate calls after a single element of the list (size N) is updated: this does not require to call N times shouldComponentUpdate

Further implementation details are shared in this ImmutableJs github issue

1
mweststrate On

In our product we also had issues related to the amount of code being rendered, and we started using observables (see this blog). This might partially solve your problem, as changing todo will no longer require the parent component that holds the list to be re-rendered (but adding still does).

It might also help you in re-rendering the list faster as your todoItem components could just return false when the props change on shouldComponentUpdate.

For further performance improvements when rendering the overview, I think your tree / paging idea is nice indeed. With observable arrays, each page could start to listen to array splices (using an ES7 polyfill or mobservable) in a certain range. That would introduce some administration, as these ranges might change over time, but should get you to O(log(n))

So you get something like:

var TodosContainer = React.createClass({
  componentDidMount() {
     this.props.todos.observe(function(change) {
         if (change.type === 'splice' && change.index >= this.props.startRange && change.index < this.props.endRange)
             this.forceUpdate();
     });
  },    

  renderTodo: function(todo) {
     return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
  },

  render: function() {
     var todos = this.props.todos.slice(this.props.startRange, this.props.endRange).map(this.renderTodo)
     return (
          <ReactCSSTransitionGroup transitionName="transition-todo">
                 {todos}
          </ReactCSSTransitionGroup>,
     );
  }

});

The central problem with large lists and react seems that you cannot just shift new DOM nodes into the dom. Othwerwise you won't need the 'pages' at all to partition the data in smaller chunks and you could just splice one new Todo item into the dom, as done with JQuery in this jsFiddle. You could still do that with react if you use a ref for each todo item, but that would be working around the system I think as it might break the reconciliation system?

0
Kornelius On

Recently I had a performance bottleneck trying to render a table with 500+ records, the reducers were immutable and I was using reselect to memoize the complex selectors, after pulling some hair, I found the problem was solved memoizing all the selectors.