我怎样才能加速生成巨大 DOM 的 React render() 方法

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

本文关键字:React DOM render 方法 巨大 加速      更新时间:2023-09-26

HtmlTable组件

想象一下一个简单的 HtmlTable React 组件。它根据通过 prop 传递给它的二维数组渲染数据data还可以通过 rowCountcolCount 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 <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 prop,它是行索引到行高的映射:

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

因此,如果用户传递值为 {1: 10}rowHeights prop,我们只需要更新一行 - 第二行。

性能问题

但是,为了执行差异,React 必须重新运行整个渲染方法并重新创建数以万计的<tr>。这对于大型数据集来说非常慢。

我想过使用 shouldComponentUpdate ,但它无济于事——瓶颈发生在我们甚至尝试更新<tr>之前。瓶颈发生在重新创建整个表以执行差异期间。

我想到的另一件事是缓存render结果,然后splice更改的行,但这似乎完全违背了使用 React 的目的。

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

编辑:显然,缓存是要走的路...例如,这是对 React 的 Github 中类似问题的讨论。React Virtualized似乎正在使用单元格缓存(尽管我可能遗漏了一些东西(。

编辑2:不是真的。存储和重用组件的"标记"仍然很慢。其中大部分来自协调 DOM,这是我应该预料到的。好吧,现在我完全迷失了。这是我为准备"标记"所做的:

    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
    }

对于这种特殊情况,我建议其他人提到的反应虚拟化或固定数据表。这两个组件都将通过延迟加载数据来限制 DOM 交互,即仅呈现表中可见的部分。

更一般地说,MobX 文档有一个关于反应性能的优秀页面。请检查一下。以下是要点。

1.使用许多小组件

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

observer允许组件独立于其父级进行渲染

2. 在专用组件中渲染列表

在渲染大型集合时尤其如此。众所周知,React 在渲染大型集合方面非常糟糕,因为协调器必须在每次集合更改时评估集合生成的组件。因此,建议使用仅映射集合并呈现它的组件,而不呈现任何其他组件:

3. 不要使用数组索引作为键

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

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

来源:

  • 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

将每一行作为子组件,并将 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;
   }
}

您应该调查如何在工作线程(或其池(中执行协调并将结果传输回主线程。有一个项目试图做到这一点,因此您可以尝试它们的实现。

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

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

  1. 生成表,将类名或 ID 添加到<tr>
  2. 生成样式表并将它们注入<head>

。重复 2.在不更改表的情况下执行步骤

(只需使用 js 生成 CSS 文本(

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

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

你可以使用一些非常简单的js来生成<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);
我知道

这个问题并不那么简单。但是,如果它只与样式相关,则将行样式也保留在父表标签中。

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