React 渲染性能优化

性能优化

在React内部已经使用了许多巧妙的技术来最小化由于Dom变更导致UI渲染所耗费的时间。对于很多应用来说,使用React后无需太多工作就会让客户端执行性能有质的提升。然而,还是很其他更多的办法来加速React程序。

使用生产模式来构建应用

如果在开发和使用的过程中感觉了React应用有明显的性能问题,请先确认是否已经构建了压缩后的生产包:

  • 在单页面用中,打包之后的生产文件应该是.min.js版本。
  • 对于Brunch(html打包工具:http://brunch.io/),打包命令需要包含-p标记。
  • 对于Browserify(UMD规范打包工具:http://browserify.org/),打包时需要增加生产配置参数—— NODE_ENV=production
  • 对于在创建React App时,需要执行 npm run build 命令,并按照说明操作。
  • 对于Rollup(JavaScript代码高效压缩工具:https://rollupjs.org/),生产打包时需要在 commonjs 插件之前使用 replace 插件:
    plugins: [
      require('rollup-plugin-replace')({
        'process.env.NODE_ENV': JSON.stringify('production')
      }),
      require('rollup-plugin-commonjs')(),
      // ...
    ]

    可以在这里看到 一个完整的例子:see this gist

  • 使用Webpack打包,需要在打生产包的配置脚本中增加以下配置和插件:

    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    new webpack.optimize.UglifyJsPlugin()

切记不要将开发模式的包发布到生产环境,因为开发包中额外包含了许多用于辅助的测试的信息,无论在加载还是执行时,它都比较慢。

使用chrome分析组件的渲染时间线

在开发模式中下你可以直接在chrome的性能工具中看到组件是如何装载、更新和卸载的。例如下面的图片展示的效果:

React 渲染性能优化

在chrome中按照以下步骤执行:

  1. 使用?react_perf作为url参数(例如:http://localhost:3000/?react_perf)
  2. 打开chrome的开发工具Timeline,然后点击Record(左上角的红色按钮)。
  3. 执行你要监控的操作。请不要记录超过20秒,这可能会导致chrome假死。
  4. 停止记录。
  5. React事件将会批量记录在User Timing标签里。

关于分析的数据,需要明确的是:渲染的时间只是一个相对的参考值,在构建成生产包之后,渲染的速度会更快。尽管如此,这些数据仍然能够帮助我们分析是否有不相关的UI被错误的更新,以及UI更新的频率和深度。

目前只有Chrome、Edge和IE支持这个特性,但是官方正在使用User Timing API 标准 让更多浏览器支持这个特性。

手工避免重复渲染

React构建和维护了一个内部的虚拟Dom,这个Dom和真实的UI是相互映射的关系,他包含从用户自定义组件中返回的各种React元素。这个虚拟的Dom使得React可以避免重复渲染相同的Dom节点并在访问存在的节点时直接使用React的虚拟层数据,这样设计的原因是重复渲染浏览器或web view的UI比操作一个JavaScript的对象要慢许多。在React Native也采用同样的处理方式。

当组件的props和state变更时,React会将最新返回的元素与之前旧的元素进行对比来确定是否真的需要重新渲染真实的Dom。当他们不相等时,React会更新真实的Dom。

在某些情况下,可以在自定义组件中重载shouldComponentUpdate方法来加速触发渲染的比对的过程。该方法的默认实现返回参数为true,此时React将按照原来的方式进行比对和渲染:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

如果在某些情况下能够清晰的明确组件不需要重新渲染,可以在 shouldComponentUpdate 方法中返回 false,这样会让让组件跳过整个渲染过程,包括不再调用当前组件和子组件的render()方法。

shouldComponentUpdate 的执行过程

下面是一个组件结构树。图中,“SCU”表示 shouldComponentUpdate 方法返回的值(绿色true,红色fasle),“vDOMEq”表示React的匹配是否一致(绿色true,红色fasle),有颜色的红圈表示是否执行了UI重绘(绿色表示没重绘,红色表示执行重绘)。

React 渲染性能优化

在C2组件中,shouldComponentUpdate 方法返回了false,所以React不会判断是否需要重新渲染C2并且不执行render()方法, 因此在C4和C5中不再执行shouldComponentUpdate 方法。

对于C1和C3,shouldComponentUpdate 都返回了true,所以React必须对着2个组件进行比对。对于C6,shouldComponentUpdate 返回true,而且比对的结果是需要UI重绘,因此C6会更新他们的真实Dom。

还有一个值得关心的组件是C8,React在这个组件中执行了render()方法,但是由于虚拟Dom并没有发生变更,前后比对一致,所以并没有发生真实Dom渲染。

在整个过程中React仅仅变更了C6组件的UI样式,C8由于前后虚拟Dom一致因此没有真正的执行UI渲染。C2、C2的子组件以及C7没有执行render()方法。

一个shouldComponentUpdate的例子

在例子中,当props.color和state.count发生变更时进行UI渲染,我们在 shouldComponentUpdate 方法中进行检查:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    //只判断props.color和nextState.count是否变更,其他情况均不渲染
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

在这段代码中,shouldComponentUpdate 仅仅检查 props.color和 state.count是否发生变更,如果他们的值没有修改,组件将不会发生任何更新。在实际使用中,组件往往比这个复杂,我们可以使用类似于“浅比较”(关于浅比较可以参看: Shallow Compare)的模式来比对所有的属性或状态是否发生变更。React提供了这个模式的一个实现组件,只要让组件继承自 React.PureComponent即可。我们可以将代码进行下面的修改:

//继承自React.PureComponent
class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

在大部分情况下,只要使用 React.PureComponent 就可以代替我们自己重载 shouldComponentUpdate方法,但是它仅仅适用于“浅比较”,所以这个组件不适用于props和state数据发生突变的情况。

附:数据突变(mutated)是指变量的引用没有改变(指针地址未改变),但是引用指向的数据发生了变化(指针指向的数据发生变更)。例如const x = {foo:'foo'}。x.foo='none' 就是一个突变。

在更复杂的数据结构中还会存在一些问题。例如下面的代码,我们希望ListOfWords 组件将words参数渲染成一个逗号分隔的字符串,而父组件监控点击事件,每次点击都会增加一个单词到列表中,但是下面的代码并不会正确工作:

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 这段内容会导致代码不按照预期工作。
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

导致代码无法正常工作的原因是 PureComponent 仅仅对 this.props.words的新旧值进行“浅比较”。在words值在handleClick中被修改之后,即使有新的单词被添加到数组中,但是this.props.words的新旧值在进行比较时是一样的(引用对象比较),因此 ListOfWords 一直不会发生渲染。

非突变数据的价值

有一个简单的方法预防上面提到的问题,就是在使用prop和state时防止数据发生突变。例如下面的例如,我们用数组的concat方法来代替等号“=”,这样在concat后会产生一个新的数组赋值给this.state.words:

handleClick() {
  this.setState(prevState => ({
    words: prevState.words.concat(['marklar'])
  }));
}

ES6支持列表扩展语法,因此我们更容易在es6中实现非突变的数据赋值,例如:

handleClick() {
  this.setState(prevState => ({
    words: [...prevState.words, 'marklar'],
  }));
};

可以重写传统的赋值语句防止对象中的数据发生数据突变。下面的例子有一个名为 colormap 的对象,我们想在修改 colormap.right 的值时渲染组件,我们可以这样重写组件:

function updateColorMap(colormap) {
  colormap.right = 'blue'; //浅拷贝,指针地址未变,数据发生变化。
}

可以使用 Object.assign 方法来防止数据突变:

function updateColorMap(colormap) {
  // 深拷贝,修改返回对象的地址
  return Object.assign({}, colormap, {right: 'blue'});
}

修改后 updateColorMap 方法返回一个新的实例。需要注意的是某些浏览器不支持 Object.assign方法,我们需要使用polyfill(差异化抹平,比如我们引入了babel-polyfill)来解决这个问题。

有一个新的JavaScript方案是使用 扩展传播特性(见 object spread properties )来解决数据突变问题,实现如下:

function updateColorMap(colormap) {
  return {...colormap, right: 'blue'};
}

如果是构建React的App应用,那么以上方法都能够很好的支持,如果是在浏览器环境使用,需要引入polyfill机制。

使用不可变的数据结构

Immutable.js 是解决数据突变问题的另外一种解决方案。它提供不可变、持久化的集合。集合包含下列结构:

  • Immutable:一旦数据被创建,改集合不能在任何其他地方修改。
  • Persistent:可以从已有的的数据集合(例如set)来创建新的数据集合。在创建新的数据集合后,已有的数据集合依然有效。
  • 结构分享(Structural Sharing):使用和原始数据尽可能相似的结构创建新的数据集合,并将复制降至最低,尽可能的提高效率。

数据结构不可变的特性使跟踪数据变化变得很简单。任何变更将始终导致创建一个新的对象,所以我们只需要检查引用(指针地址)是否已经被修改即可确定数据是否已经修改。例如在常规的JavaScript代码中:

const x = { foo: "bar" };
const y = x;
y.foo = "baz";
x === y; // true

尽管y的值已经被修改,但是它和x都是同一个引用(指向相同的地址),因此最后的比较语句会返回true。我们可以使用 immutable.js来修改代码:

const SomeRecord = Immutable.Record({ foo: null});
const x = new SomeRecord({ foo: 'bar'});
const y = x.set('foo', 'baz');
x === y; // false

在这个例子中,由于x突变时使用了新的引用,我们可以安全的假设x已经发生改变。

还有两个库可以帮我们构建不可变数据: seamless-immutable and immutability-helper

不可变的数据结构为我们跟踪数据对象变更提供了更加简便的方式,这是我们快速实现shouldComponentUpdate方法的基础。使用不可变数据后,可以为React提供不错的性能提升。

上一篇:2021-05-31


下一篇:React 高阶组件传递Forwarding Refs