React mounting performance differences
I've been creating tables in React+Redux in a simple and functional way. I've never really bothered with the performance since it’s been mounting fast enough when there are only a few rows. But I recently was putting together a table of people which could be up to 1000+ rows, and that was enough to cause me to stop and think about performance implications. I had a hunch I could change how I construct the rows for some clear performance wins. That called for some quick benchmarking.
Let me setup a benchmark case: There is a redux store with 2000 people in it that needs to be displayed in a table. Something like this:
{
data: {
people_ids: [1, 2, ...],
people: {
1: {
id: 1,
firstName: 'Some 1',
lastName: 'One 1',
team: 'Team 1',
group: 'Group'
},
2: {
id: 2,
firstName: 'Some 2',
lastName: 'One 2',
team: 'Team 2',
group: 'Group'
},
...
}
}
}
Each table row should display name, team and group, and when you click a row it navigates to a page showing that person. The way I would normally construct it is like this:
// TableV1.js
import React, { Component } from 'react';
import {connect} from 'react-redux';
import TableRowV1 from './TableRow';
function mapStateToProps(state) {
const ids = state.data.people_ids;
return {ids};
}
class Table extends Component {
render() {
const {ids} = this.props;
return <table>
<thead>
<tr>
<th>Name</th>
<th>Team</th>
<th>Group</th>
</tr>
</thead>
<tbody>
{ids.map(id => <TableRow key={id} id={id} />)}
</tbody>
</table>;
}
}
export default connect(mapStateToProps)(Table);
// TableRowV1.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {push} from 'react-router-redux';
function mapStateToProps(state, ownProps) {
const person = state.data.people[ownProps.id];
return {person};
}
class TableRow extends Component {
navigate = () => {
this.props.dispatch(push(`/person/${this.props.person.id}`));
}
render() {
const {person} = this.props;
return <tr onClick={this.navigate}>
<td>{person.firstName} {person.lastName}</td>
<td>{person.team}</td>
<td>{person.group}</td>
</tr>;
}
}
export default connect(mapStateToProps)(TableRow);
Assume some wrapper component that sets up Redux, React Router, and React Router Redux. Basic idea is that there is a Table.js
component that gets the ids from the store, creates the table and table header, and finally maps over the ids to create a TableRow
for each one. The TableRow.js
component in turn uses the id that is passed to it to get the person from the store, and then renders a tr
with the person's details. It then uses push
from React Router Redux to dispatch a push action on click to navigate.
I find it a nice clean design, but the performance left something to be desired. There is a lot of work happening in each row. The mapStateToProps
function needs to run, both to mount each row and each time state changes, and there is also a need to create a navigate
method for each instance of TableRow
. I thought it would be better to avoid as much work as possible in the TableRow
. That means it would be better to move as much of the work up to the parent Tabel
component and then pass down everything needed as props to each row.
After hoisting out as much as possible up to Table
the code looked like this
// TableV2.js
import React, { Component } from 'react';
import {connect} from 'react-redux';
import {push} from 'react-router-redux';
import TableRow from './TableRow';
function mapStateToProps(state) {
const ppl = state.ui.ids.map(id => state.ui.dict[id]);
return {ppl};
}
class Table extends Component {
constructor(props) {
super(props);
this.navigate = this.navigate.bind(this);
}
navigate(id) {
this.props.dispatch(push(`/person/${id}`));
}
render() {
const {ppl} = this.props;
return <table>
<thead>
<tr>
<th>Name</th>
<th>Team</th>
<th>Group</th>
</tr>
</thead>
<tbody>
{ppl.map(p => <TableRow key={p.id} person={p} navigate={this.navigate} />)}
</tbody>
</table>
}
}
export default connect(mapStateToProps)(Table);
// TableV2.js
import React, {PureComponent} from 'react';
class TableRow extends PureComponent {
constructor(props) {
super(props);
this.handleNav = this.handleNav.bind(this);
}
handleNav() {
this.props.navigate(this.props.person.id);
}
render() {
const {person} = this.props;
return <tr onClick={this.handleNav}>
<td>{person.firstName} {person.lastName}</td>
<td>{person.team}</td>
<td>{person.group}</td>
</tr>
}
}
export default TableRow;
Besides moving all the redux action up to Table
, I also made TableRow
a PureComponent
, and I bind
this
to handleNav
in the constructor to avoid having to create a new method for each instance of the component. The PureComponent
not make a big deal for initial mount but can save some rendering time when something causes table to re-render.
So, enough with the details. What was the performance difference? As test data I generated 2000 random people. To measure the performance I used the Chrome Performance Profile, which with User Timings makes it easy to get performance details on React components. I also set CPU slowdown to 4x, to help put emphasis the performance difference.
The results were clear: Time to mount for V1 was 2.63 seconds but the time to mount was 1.03 second. Honestly, the difference was bigger than I thought. That's a solid 60% improvement. Of course, the difference would not be meaningful with only a few rows in the table, but it will amount to a something noticeable once the table gets bigger. I guess I'll have to rethink how I build tables in React in the future.