HtmlTable
componentImagine 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;
}
}
rowHeights
propsNow 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.
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 splice
ing 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
}
Once React knows which virtual DOM objects have changed, then React updates only those objects, in the real DOM. This makes the performance far better when compared to manipulating the real DOM directly. This makes React standout as a high performance JavaScript library.
To optimize React rendering, you need to make sure that components receive only necessary props. It will let you control the CPU consumption and avoid over-rendering unnecessary features. The solution is to create a functional component that will collect all props and redistribute them to other components.
Another way to render a large amount of data is with infinite scroll. Infinite scroll involves appending data to the end of the page as you scroll down the list. When the page initially loads, only a subset of data is loaded. As you scroll down the page, more data is appended.
For this particular case I would recommend react-virtualized or fixed-data-table as mentioned by others. Both components will limit DOM interaction, by lazy loading data, i.e. only render the portion of the table that is visible.
Speaking more generally, the MobX documentation has an excellent page on react performance. Please check it out. Here are the bullet points.
mobx-react's
@observer
components will track all values they use and re-render if any of them changes. So the smaller your components are, the smaller the change they have to re-render; it means that more parts of your user interface have the possibility to render independently of each other.
observer
allows components to render independently from their parent
This is especially true when rendering big collections. React is notoriously bad at rendering large collections as the reconciler has to evaluate the components produced by a collection on each collection change. It is therefore recommended to have components that just map over a collection and render it, and render nothing else:
Don't use array indexes or any value that might change in the future as key. Generate id's for your objects if needed. See also this blog.
When using mobx-react it is recommended to dereference values as late as possible. This is because MobX will re-render components that dereference observable values automatically. If this happens deeper in your component tree, less components have to re-render.
This tip applies to React in general and libraries using PureRenderMixin especially, try to avoid creating new closures in render methods.
Example (untested)
import { observer } from 'mobx-react';
import { observable } from 'mobx';
const HtmlTable = observer(({
data,
}) => {
return (
<table>
<TBody rows={data} />
</table>
);
}
const TBody = observer(({
rows,
}) => {
return (
<tbody>
{rows.map((row, i) => <Row row={row} />)}
</tbody>
);
});
const Row = observer(({
row,
}) => {
return (
<tr key={row.id} style={{height: row.rowHeight + 'px'}}>
{row.cols.map((cell, i) =>
<td key={cell.id}>{cell.value}</td>
)}
</tr>
);
});
class DataModel {
@observable rows = [
{ id: 1, rowHeight: 10, cols: [{ id: 1, value: 'one-1' }, { id: 2, value: 'one-2' }] },
{ id: 2, rowHeight: 5, cols: [{ id: 1, value: 'two-1' }, { id: 2, value: 'two-2' }] },
{ id: 3, rowHeight: 10, cols: [{ id: 1, value: 'three-1' }, { id: 2, value: 'three-2' }] },
];
@observable rowLimit = 10;
@observable colLimit = 10;
@computed
get limitedRows() {
return this.limitData(this.rows, this.rowLimit).map(r => {
return { id: r.id, cols: this.limitData(r.col, this.colLimit) };
});
}
limitData(data, limit) {
return limit ? data.slice(0, limit) : data;
}
}
const data = new DataModel();
React.render(<HtmlTable data={data.limitedRows} />, document.body);
sources:
Make each row as a subcomponent and pass the props to that subcomponent. That subcomponent will have its own state and can change without affecting all the rows
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 (<Row cols={cols} key={i}/>)
})}</tbody>
</table>
}
shouldComponentUpdate() {
return false;
}
limitData(data, limit) {
return limit ? data.slice(0, limit) : data;
}
}
You should investigate on how to perform reconciliation within a worker (or a pool thereof) and transfer the result back to your main thread. There exists a project that attempts to do just that, so you can experiment with their implementation.
Please note that I belong to the majority that has never encountered such requirements with React as yourself, so I can't provide any implementation details or further hints, but I believe web-perf/react-worker-dom
may be a good place to start to start exploring and discovering similar solutions.
The problem here is to change the inline styles.. maybe just use the styles for the particular <tr>
outside of the table..
<tr>
<head>
... repeat the 2. step without changing the table
(just generate the css text with js)
.tr-0, #tr-0, .tr-1, ... { height: 10px }
...
I did't tested it, its just an idea, but I think this could be a way to go, if you don't want to touch the generated table after rendering..
You can use some really simple js to generate <style>
and put them into the head:
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = '.tr-0 { height: 10px; }';
// write some generator for the styles you have to change and add new style tags to the head...
document.getElementsByTagName('head')[0].appendChild(style);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With