Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I speed up a React render() method that generates a huge DOM?

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
    }
like image 263
sbichenko Avatar asked Jul 24 '16 10:07

sbichenko


People also ask

What makes Reactjs faster virtual DOM?

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.

What makes Reactjs performance faster?

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.

How do you handle a large amount of data in React?

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.


4 Answers

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.

1. Use many small components

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

2. Render lists in dedicated components

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:

3. Don't use array indexes as keys

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.

4. Dereference values lately

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.

5. Bind functions early

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:

  • https://mobxjs.github.io/mobx/best/react-performance.html
  • https://github.com/mobxjs/mobx-react#faq
  • https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
like image 118
Kyle Finley Avatar answered Oct 19 '22 11:10

Kyle Finley


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;
   }
}
like image 20
Piyush.kapoor Avatar answered Oct 19 '22 11:10

Piyush.kapoor


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.

like image 2
Filip Dupanović Avatar answered Oct 19 '22 11:10

Filip Dupanović


The problem here is to change the inline styles.. maybe just use the styles for the particular <tr> outside of the table..

  1. Generate the table, add classNames or ids to the <tr>
  2. Generate the stylesheets and inject them into the <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);
like image 2
webdeb Avatar answered Oct 19 '22 11:10

webdeb