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
3 Answers
Reset to default 9If 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)
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.
There is another warning which you can easily fix by changing event handler on
<input type="checkbox" />
inside<TodoItem />
fromonClick
toonChange
:<input onClick={this.updateCompletedState} type="checkbox" checked={this.state.data.completed ? 'checked' : ''} />
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".