React 性能优化

Flying
2019-01-25 / 0 评论 / 182 阅读 / 正在检测是否收录...

React 通过虚拟 DOM、高效的 Diff 算法等技术极大地提高了操作 DOM 的效率。在大多数场景下,我们是不需要考虑 React 程序的性能问题的,但只要是程序,总会有一些优化的措施。本章就来介绍一下 React 中常用的性能优化方式。

thunder-react.svg

使用生产版本

这是性能优化的一个基本原则,也是很容易被忽视的一个原则。我们使用 CRA 脚手架创建的项目,在以 npm run start 启动时,使用的 React 是开发环境版本的 React 库,包含大量警告消息,以帮助我们在开发过程中避免一些常见的错误,比如组件 props 类型的校验等。开发环境版本的库不仅体积更大,而且执行速度也更慢,显然不适合在生产环境中使用。那么,如何构建生产环境版本的 React 库呢?对于 CRA 脚手架创建的项目,只需要执行 npm run build,就会构建生产环境版本的 React 库。其原理是,一般第三方库都会根据 process.env.NODE_ENV 这个环境变量决定在开发环境和生产环境下执行的代码有哪些不同,当执行 npm run build 时,构建脚本会把 NODE_ENV 的值设置为 production,也就是会以生产环境模式编译代码。

如果不是使用 create-react-app 脚手架创建的项目,而是完全自己编写 Webpack 的构建配置,那么在执行生产环境的构建时,就需要在 Webpack 的配置项中包含以下插件的配置:

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify('production')
    }
  }),
  new UglifyJSPlugin()
  //...
];

当 NODE_ENV 等于 production 时,不仅是 React,项目中使用到的其他库也会执行生产环境版本的构建。但一定要注意,在开发过程中不要执行这项设置,因为它会让你失去很多重要的调试信息,并且代码的编译速度也会变慢。

虚拟化长列表

如果你的应用渲染了长列表(上百甚至上千的数据),我们推荐使用“虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。

react-windowreact-virtualized 是热门的虚拟滚动库。 它们提供了多种可复用的组件,用于展示列表、网格和表格数据。如果你想要一些针对你的应用做定制优化,你也可以创建你自己的虚拟滚动组件。

避免不必要的组件渲染

当组件的 props 或 state 发生变化时,组件的 render 方法会被重新调用,返回一个新的虚拟 DOM 对象。但在一些情况下,组件是没有必要重新调用 render 方法的。例如,父组件的每一次 render 调用都会触发子组件 componentWillReceiveProps 的调用,进而子组件的 render 方法也会被调用,但是这时候子组件的 props 可能并没有发生改变,改变的只是父组件的 props 或 state,所以这一次子组件的 render 是没有必要的,不仅多了一次 render 方法执行的时间,还多了一次虚拟 DOM 比较的时间。

React 组件的生命周期方法中提供了一个 shouldComponentUpdate 方法,这个方法的默认返回值是 true,如果返回 false,组件此次的更新将会停止,也就是后续的 componentWillUpdaterender 等方法都不会再被执行。我们可以把这个方法作为钩子,在这个方法中根据组件自身的业务逻辑决定返回 true 还是 false,从而避免组件不必要的渲染。例如,我们通过比较 props 中的一个自定义属性 item,决定是否需要继续组件的更新过程,代码如下:

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (nextProps.item === this.props.item) {
      return false;
    }
    return true;
  }
  // ...
}
注意:示例中对 item 的比较是通过 === 比较对象的引用,所以即使两个对象的引用不相等,它们的内容也可能是相等的。最精确的比较方式是遍历对象的每一层级的属性分别比较,也就是进行深比较,但 shouldComponentUpdate 被频繁调用,如果 props 和 state 的对象层级很深,深比较对性能的影响就比较大。一种折中的方案是,只比较对象的第一层级的属性,也就是执行浅比较。例如下面两个对象:
const item = { foo, bar };
const nextItem = { foo, bar };

执行浅比较会使用 === 比较 item.foonextItem.fooitem.barnextItem.bar,而不会继续比较 foo、bar 的内容。React 中提供了一个 PureComponent 组件,这个组件会使用浅比较来比较新旧 props 和 state,因此可以通过让组件继承 PureComponent 来替代手写 shouldComponentUpdate 的逻辑。但是,使用浅比较很容易因为直接修改数据而产生错误,例如:

class NumberList extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      numbers: [1, 2, 3, 4]
    };
    this.handleClick = this.handleClick.bind(this);
  }
  // numbers 中新加一个数值
  handleClick() {
    const numbers = this.state.numbers;
    // 直接修改 numbers 对象
    numbers.push(numbers[numbers.length - 1] + 1);
    this.setState({ numbers: numbers });
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        {this.state.numbers.map((item) => (
          <div>{item}</div>
        ))}
      </div>
    );
  }
}

点击 ButtonNumberList 并不会重新调用 render,因为 handleClick 中是直接修改 this.state.numbers` 这个数组的,this.state.numbers 的引用在 setState 前后并没有发生改变,所以 shouldComponentUpdate` 会返回 false,从而终止组件的更新过程。在第 4 章深入理解组件 state 中,我们讲到要把 state 当作不可变对象,一个重要的原因就是为了提高组件 state 比较的效率。对于不可变对象来说,只需要比较对象的引用就能判断 state 是否发生改变。

列表使用 key

列表元素定义了 key,React 会根据 key 索引元素,在 render 前后,拥有相同 key 值的元素是同一个元素,例如前面举过的例子:

// render 前
<ul>
  <li key="first" >first</li>
  <li key="second">second</li>
</ul>

// render 后
<ul>
  <li key="third">third</li>
  <li key="first">first</li>
  <li key="second">second</li>
</ul>

定义 key 之后,React 并不会“傻瓜式”地按顺序依次更新每一个 li 元素:把第一个 li 元素更新为 third,把第二个 li 元素更新为 first,最后创建一个新的 li 元素,内容为 second。有了 key 的索引,React 知道 firstsecond 这两个 li 元素并没有发生变化,而只会在这两个 li 元素前面插入一个内容为 third 的 li 元素。可见,key 的使用减少了 DOM 操作,提高了 DOM 更新效率。当列表元素数量很多时,key 的使用更显得重要。

总结

性能优化方法是最常用的这几种方法,其中使用生产版本是项目中必须采用的,使用虚拟化长列表和 key 也推荐在项目中采用。通过重写 shouldComponentUpdate 方法避免不必要的组件渲染,这在项目开始阶段是可以不必在意的,大多数情况下,组件只是重复调用 render 方法对于性能的影响并不大。当发现项目确实存在性能问题时,再考虑通过这种方式进行优化也不迟。请大家记住,过早的优化并不是
一件好事。

3

评论 (0)

取消