The HtmlTable component
Imagine a simple HtmlTable React component. It renders data based on a 2-dimensional array passed to it via data prop, and it can also limit the number of columns and rows via rowCount and colCount props. Also, we need the component to handle huge arrays of data (tens of thousands of rows) without pagination.
class HtmlTable extends React.Component {
render() {
var {rowCount, colCount, data} = this.props;
var rows = this.limitData(data, rowCount);
return <table>
<tbody>{rows.map((row, i) => {
var cols = this.limitData(row, colCount);
return <tr key={i}>{cols.map((cell, i) => {
return <td key={i}>{cell}</td>
})}</tr>
})}</tbody>
</table>
}
shouldComponentUpdate() {
return false;
}
limitData(data, limit) {
return limit ? data.slice(0, limit) : data;
}
}
The rowHeights props
Now we want to let the user change the row heights and do it dynamically. We add a rowHeights prop, which is a map of row indices to row heights:
{
1: 100,
4: 10,
21: 312
}
We change our render method to add a style prop to <tr> if there's a height specified for its index (and we also use shallowCompare for shouldComponentUpdate):
render() {
var {rowCount, colCount, data, rowHeights} = this.props;
var rows = this.limitData(data, rowCount);
return <table>
<tbody>{rows.map((row, i) => {
var cols = this.limitData(row, colCount);
var style = rowHeights[i] ? {height: rowHeights[i] + 'px'} : void 0;
return <tr style={style} key={i}>{cols.map((cell, i) => {
return <td key={i}>{cell}</td>
})}</tr>
})}</tbody>
</table>
}
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
So, if a user passes a rowHeights prop with the value {1: 10}, we need to update only one row -- the second one.
The performance problem
However, in order to do the diff, React would have to rerun the whole render method and recreate tens of thousands of <tr>s. This is extremely slow for large datasets.
I thought about using shouldComponentUpdate, but it wouldn't have helped -- the bottleneck happened before we even tried to update <tr>s. The bottleneck happens during recreation of the whole table in order to do the diff.
Another thing I thought about was caching the render result, and then spliceing changed rows, but it seems to defeat the purpose of using React at all.
Is there a way to not rerun a "large" render function, if I know that only a tiny part of it would change?
Edit: Apparently, caching is the way to go... For example, here's a discussion of a similar problem in React's Github. And React Virtualized seems to be using a cell cache (though I might be missing something).
Edit2: Not really. Storing and reusing the "markup" of the component is still slow. Most of it comes from reconciling the DOM, which is something I should've expected. Well, now I'm totally lost. This is what I did to prepare the "markup":
componentWillMount() {
var {rowCount, colCount, data, rowHeights={}} = this.props;
var rows = this.limitData(data, rowCount);
this.content = <table>
<tbody>{rows.map((row, i) => {
var cols = this.limitData(row, colCount);
var style = rowHeights[i] ? {height: rowHeights[i] + 'px'} : void 0;
return <tr style={style} key={i}>{cols.map((cell, i) => {
return <td key={i}>{cell}</td>
})}</tr>
})}</tbody>
</table>
}
render() {
return this.content
}
TableRowComponent. Then you should be able to just rerender that row. There's no way around the DOM having to do a reflow but it would at least speed up incremental renders. It won't do anything for the initial render though. Pagination would be needed for that I think. - ivarnishouldComponentUpdate, if the style for that row hasn't changed, returnfalse. Therefore an update of{1: 10}would only runrender()for that one row. - David Gilbertson