最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - ReactJS. Quite slow when rendering and updating a simple list of 1500 <li> elements. I thought Virtua

programmeradmin4浏览0评论

I was really disappointed by the performance I got on the following simple ReactJS example. When clicking on an item, the label (count) gets updated accordingly. Unfortunately, this takes roughly ~0.5-1 second to get updated. That's mainly due to "re-rendering" the entire todo list.

My understanding is that React's key design decision is to make the API seem like it re-renders the whole app on every update. It is supposed take the current state of the DOM and compare it with the target DOM representation, do a diff and update only the things that need to get updated.

Am I doing something which is not optimal? I could always update the count label manually (and the state silently) and that will be an almost instant operation but that takes away the point of using ReactJS.

/** @jsx React.DOM */

TodoItem = React.createClass({

    getDefaultProps: function () {
        return {
            completedCallback: function () {
                console.log('not callback provided');
            }
        };
    },
    getInitialState: function () {
        return this.props;
    },

    updateCompletedState: function () {
        var isCompleted = !this.state.datapleted;
        this.setState(_.extend(this.state.data, {
            completed: isCompleted
        }));
        this.propspletedCallback(isCompleted);
    },

    render: function () {
        var renderContext = this.state.data ?
            (<li className={'todo-item' + (this.state.datapleted ? ' ' + 'strike-through' : '')}>
                <input onClick={this.updateCompletedState} type="checkbox" checked={this.state.datapleted ? 'checked' : ''} />
                <span onClick={this.updateCompletedState} className="description">{this.state.data.description}</span>
            </li>) : null;

        return renderContext;
    }
});


var TodoList = React.createClass({
    getInitialState: function () {
        return {
            todoItems: this.props.data.todoItems,
            completedTodoItemsCount: 0
        };
    },

    updateCount: function (isCompleted) {
        this.setState(_.extend(this.state, {
            completedTodoItemsCount: isCompleted ? this.statepletedTodoItemsCount + 1 : this.statepletedTodoItemsCount - 1
        }));
    },
    
    render: function () {
        var updateCount = this.updateCount;
        return (
            <div>
                <div>count: {this.statepletedTodoItemsCount}</div>
                <ul className="todo-list">
                    { this.state.todoItems.map(function (todoItem) {
                        return <TodoItem data={ todoItem } completedCallback={ updateCount } />
                    }) }
                </ul>
            </div>
        );
    }
});

var data = {todoItems: []}, i = 0;

while(i++ < 1000) {
	data.todoItems.push({description: 'Comment ' + i, completed: false});
}

React.renderComponent(<TodoList data={ data } />, document.body);
<script src=".js"></script>

I was really disappointed by the performance I got on the following simple ReactJS example. When clicking on an item, the label (count) gets updated accordingly. Unfortunately, this takes roughly ~0.5-1 second to get updated. That's mainly due to "re-rendering" the entire todo list.

My understanding is that React's key design decision is to make the API seem like it re-renders the whole app on every update. It is supposed take the current state of the DOM and compare it with the target DOM representation, do a diff and update only the things that need to get updated.

Am I doing something which is not optimal? I could always update the count label manually (and the state silently) and that will be an almost instant operation but that takes away the point of using ReactJS.

/** @jsx React.DOM */

TodoItem = React.createClass({

    getDefaultProps: function () {
        return {
            completedCallback: function () {
                console.log('not callback provided');
            }
        };
    },
    getInitialState: function () {
        return this.props;
    },

    updateCompletedState: function () {
        var isCompleted = !this.state.data.completed;
        this.setState(_.extend(this.state.data, {
            completed: isCompleted
        }));
        this.props.completedCallback(isCompleted);
    },

    render: function () {
        var renderContext = this.state.data ?
            (<li className={'todo-item' + (this.state.data.completed ? ' ' + 'strike-through' : '')}>
                <input onClick={this.updateCompletedState} type="checkbox" checked={this.state.data.completed ? 'checked' : ''} />
                <span onClick={this.updateCompletedState} className="description">{this.state.data.description}</span>
            </li>) : null;

        return renderContext;
    }
});


var TodoList = React.createClass({
    getInitialState: function () {
        return {
            todoItems: this.props.data.todoItems,
            completedTodoItemsCount: 0
        };
    },

    updateCount: function (isCompleted) {
        this.setState(_.extend(this.state, {
            completedTodoItemsCount: isCompleted ? this.state.completedTodoItemsCount + 1 : this.state.completedTodoItemsCount - 1
        }));
    },
    
    render: function () {
        var updateCount = this.updateCount;
        return (
            <div>
                <div>count: {this.state.completedTodoItemsCount}</div>
                <ul className="todo-list">
                    { this.state.todoItems.map(function (todoItem) {
                        return <TodoItem data={ todoItem } completedCallback={ updateCount } />
                    }) }
                </ul>
            </div>
        );
    }
});

var data = {todoItems: []}, i = 0;

while(i++ < 1000) {
	data.todoItems.push({description: 'Comment ' + i, completed: false});
}

React.renderComponent(<TodoList data={ data } />, document.body);
<script src="http://fb.me/react-js-fiddle-integration.js"></script>

jsFiddle link, just in case: http://jsfiddle.net/9nrnz1qm/3/

Share Improve this question edited Jan 15, 2015 at 13:06 Luboš Turek 6,5959 gold badges42 silver badges50 bronze badges asked Jan 14, 2015 at 21:36 skay-skay- 1,5763 gold badges16 silver badges26 bronze badges 4
  • I get 0.13s with the production build, any better for you?. Diffing 4500 elements takes some time. The only good solutions here involve not rendering 1500 TodoItems, when you can only see 100 at most :-) – Brigand Commented Jan 14, 2015 at 22:53
  • How do you check the performance (i.e. you got 0.13s)? – skay- Commented Jan 14, 2015 at 23:07
  • I used the profiler in dev tools. It shows you a timeline of all JS execution (make sure it's in Chart mode, not Heavy or Tree). After clicking a checkbox there was about 130ms of code execution. See my answer, it actually went down quite a bit. – Brigand Commented Jan 14, 2015 at 23:10
  • Yeap.. mine is around 150-170ms – skay- Commented Jan 14, 2015 at 23:27
Add a comment  | 

3 Answers 3

Reset to default 9

If you do the following, you can cut the time down by a lot. It spends 25ms to 45ms to update for me.

  • use the production build
  • implement shouldComponentUpdate
  • update the state immutably
updateCompletedState: function (event) {
    var isCompleted = event.target.checked;
    this.setState({data: 
        _.extend({}, this.state.data, {
            completed: isCompleted
       })
    });
    this.props.completedCallback(isCompleted);
},

shouldComponentUpdate: function(nextProps, nextState){
    return nextState.data.completed !== this.state.data.completed;
},

Updated fiddle

(there are a lot of questionable things about this code, daniula points out some of them)

  1. When you are generating list of elements you should provide unique key prop for everyone. In your case:

    <ul className="todo-list">
        { this.state.todoItems.map(function (todoItem, i) {
            return <TodoItem key={i} data={ todoItem } completedCallback={ updateCount } />
        }) }
    </ul>
    

    You can find out about this mistake by warning message in browser console:

    Each child in an array should have a unique "key" prop. Check the render method of TodoList. See fb.me/react-warning-keys for more information.

  2. There is another warning which you can easily fix by changing event handler on <input type="checkbox" /> inside <TodoItem /> from onClick to onChange:

    <input onClick={this.updateCompletedState} type="checkbox" checked={this.state.data.completed ? 'checked' : ''} />
    
  3. You are doing some string concatenation to set proper className. For more readable code try using nice and simple React.addons.classSet:

    render: function () {
        var renderContext = this.state.data ?
            var cx = React.addons.classSet({
                'todo-item': true,
                'strike-through': this.state.data.completed
            });
    
            (<li className={ cx }>
                <input onChange={this.updateCompletedState} type="checkbox" checked={this.state.data.completed ? 'checked' : ''} />
                <span onClick={this.updateCompletedState} className="description">{this.state.data.description}</span>
            </li>) : null;
    
        return renderContext;
    }
    

I'm looking at where you render() the list...

<div>
   <div>count: {this.state.completedTodoItemsCount}</div>
   <ul className="todo-list">
        { this.state.todoItems.map(function (todoItem) {
            return <TodoItem data={ todoItem } completedCallback={ updateCount } />
        }) }
   </ul>
</div>

This should not be called every time a TodoItem is updated. Give the above element a surrounding div and an id like this:

return <div id={someindex++}><TodoItem
  data={ todoItem }
  completedCallback={ updateCount }
/></div>

Then simply rerender a single TodoItem as it is changed, like so:

ReactDOM.render(<TodoItem ...>, document.getElementById('someindex'));

ReactJS is supposed to be fast, yes, but you still need to stick to general programming paradigms, i.e., asking the machine to do as little as possible, thereby producing the result as fast as possible. Rerendering stuff that doesn't need to get re-rendered gets in the way of that, whether or not it's "best practice".

发布评论

评论列表(0)

  1. 暂无评论