如何加速生成巨大DOM的React render()方法?

时间:2016-07-24 10:33:32

标签: javascript performance reactjs

HtmlTable组件

想象一个简单的HtmlTable React组件。它基于通过data prop传递给它的二维数组呈现数据,并且还可以通过rowCountcolCount道具限制列数和行数。此外,我们需要该组件处理大量数据(数万行)而不需要分页。

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道具

现在我们想让用户更改行高并动态执行。我们添加一个rowHeights道具,它是行索引到行高的地图:

{
    1: 100,
    4: 10,
    21: 312
}

如果为其索引指定了高度,我们会更改render方法以向style道具添加<tr>道具(我们还对shallowCompare使用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);
    }

因此,如果用户传递值rowHeights的{​​{1}}道具,我们只需要更新一行 - 第二行。

性能问题

但是,为了做差异,React必须重新运行整个render方法并重新创建成千上万的{{​​1}} s。这对于大型数据集来说非常慢。

我考虑使用{1: 10},但它没有帮助 - 瓶颈发生在我们甚至尝试更新<tr>之前。在整个表的重新创建过程中发生了瓶颈,以便进行差异化。

我想到的另一件事是缓存shouldComponentUpdate结果,然后<tr>更改了行,但似乎完全没有使用React的目的。

有没有办法不重新运行“大”render函数,如果我知道只有一小部分函数会改变?

编辑:显然,缓存是要走的路......例如,这里的a discussion of a similar problem in React's Github.和React Virtualized似乎正在使用a cell cache(尽管我可能会遗漏东西)。

Edit2:不是。存储和重用组件的“标记”仍然很慢。其中大部分来自协调DOM,这是我应该期待的。好吧,现在我完全迷失了。这就是我准备“标记”所做的:

splice

5 个答案:

答案 0 :(得分:17)

对于这种特殊情况,我建议其他人提及react-virtualizedfixed-data-table。两个组件都将通过延迟加载数据来限制DOM交互,即仅渲染表中可见的部分。

更一般地说,MobX documentation有一个excellent page on react performance。请检查一下。以下是要点。

1。使用许多小组件

  

mobx-react@observer组件将跟踪他们使用的所有值,并在其中任何值发生更改时重新呈现。因此,组件越小,重新渲染的变化就越小;这意味着您的用户界面的更多部分可以彼此独立呈现。

     

observer允许组件独立于其父

呈现

2。在专用组件中呈现列表

  

在渲染大型集合时尤其如此。 React在渲染大型集合方面是出了名的不好,因为协调程序必须在每次集合更改时评估集合生成的组件。因此,建议使用仅映射集合并对其进行渲染的组件,并不再渲染其他内容:

3。不要将数组索引用作键

  

请勿使用数组索引或将来可能更改的任何值作为键。如果需要,为您的对象生成id。另请参阅此blog

4。最近取消引用值

  

使用mobx-react时,建议尽可能晚取消引用值。这是因为MobX将重新呈现自动取消引用可观察值的组件。如果在组件树中发生更深层次的事情,则需要重新渲染的组件越少。

5。早期绑定功能

  

这个技巧通常适用于React和使用PureRenderMixin的库,尽量避免在渲染方法中创建新的闭包。

示例(未经测试

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);

来源:

答案 1 :(得分:2)

将每一行作为子组件并将props传递给该子组件。 该子组件将具有自己的状态,并且可以在不影响所有行的情况下进行更改

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;
   }
}

答案 2 :(得分:2)

您应该调查如何在工作者(或其池)中执行协调,并将结果传回主线程。有exists a project试图做到这一点,所以你可以试验他们的实现。

请注意,我所属的大多数人从未遇到过像React这样的要求,所以我无法提供任何实施细节或进一步的提示,但我相信web-perf/react-worker-dom可能是一个好地方开始探索并发现类似的解决方案。

答案 3 :(得分:2)

这里的问题是更改内嵌样式..也许只使用表格外的特定<tr>的样式..

  1. 生成表格,将classNames或id添加到<tr>
  2. 生成样式表并将其注入<head>
  3. ...重复2.步骤而不更改表格

    (只用js生成css文本)

    .tr-0, #tr-0, .tr-1, ... { height: 10px }
    ...
    

    我没有对它进行测试,这只是一个想法,但我认为这可能是一种方法,如果你不想在渲染后触摸生成的表格。

    您可以使用一些非常简单的j来生成<style>并将它们放入头部:

    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);
    

答案 4 :(得分:1)

我知道这个问题并不那么简单。但如果它只与样式相关,那么在父表标记中也保留行的样式。

https://codepen.io/shishirarora3/pen/grddLm

class Application extends React.Component {
  renderStyle=(x,y)=> {
    console.log(x,y);
    let styleCode = `table tr:nth-child(${x}){ height: ${y} }`
    console.log(styleCode);
    return (
        <style>{ styleCode }</style>
    )
};
  render() {
    return <div>
      {this.renderStyle(1,'100px')}
      <table>
        <tr><td>1</td></tr>
        <tr><td>2</td></tr>
        <tr><td>3</td></tr>
      </table>

    </div>;
  }
}

/*
 * Render the above component into the div#app
 */
React.render(<Application />, document.getElementById('app'));