Optimizing Performance

UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。

使用生产版本

当你需要对你的 React 应用进行 benchmark,或者遇到了性能问题,请确保你正在使用压缩后的生产版本。

React 默认包含了许多有用的警告信息。这些警告信息在开发过程中非常有帮助。然而这使得 React 变得更大且更慢,所以你需要确保部署时使用了生产版本。

如果你不能确定你的编译过程是否设置正确,你可以通过安装 Chrome 的 React 开发者工具 来检查。如果你浏览一个基于 React 生产版本的网站,图标背景会变成深色:React DevTools on a website with production version of React如果你浏览一个基于 React 开发模式的网站,图标背景会变成红色:React DevTools on a website with development version of React推荐你在开发应用时使用开发模式,而在为用户部署应用时使用生产模式。

你可以在下面看到几种为应用构建生产版本的操作说明。

Create React App

如果你的项目是通过 Create React App 构建的,运行:

  1. npm run build

这段命令将在你的项目下的 build/ 目录中生成对应的生产版本。

注意只有在生产部署前才需要执行这个命令。正常开发使用 npm start 即可。

单文件构建

我们提供了可以在生产环境使用的单文件版 React 和 React DOM:

  1. <script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  2. <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

注意只有以 .production.min.js 为结尾的 React 文件适用于生产。

Brunch

通过安装 terser-brunch 插件,来获得最高效的 Brunch 生产构建:

  1. # 如果你使用 npm
  2. npm install --save-dev terser-brunch
  3. # 如果你使用 Yarn
  4. yarn add --dev terser-brunch

接着,在 build 命令后添加 -p 参数,以创建生产构建:

  1. brunch build -p

请注意,你只需要在生产构建时这么做。你不需要在开发环境中使用 -p 参数或者应用这个插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

Browserify

为了最高效的生产构建,需要安装一些插件:

  1. # 如果你使用 npm
  2. npm install --save-dev envify terser uglifyify
  3. # 如果你使用 Yarn
  4. yarn add --dev envify terser uglifyify

为了创建生产构建,确保你添加了以下转换器 (顺序很重要)

  • envify 转换器用于设置正确的环境变量。设置为全局 (-g)。
  • uglifyify 转换器移除开发相关的引用代码。同样设置为全局 (-g)。
  • 最后,将产物传给 terser 并进行压缩(为什么要这么做?)。举个例子:
  1. browserify ./index.js \
  2. -g [ envify --NODE_ENV production ] \
  3. -g uglifyify \
  4. | terser --compress --mangle > ./bundle.js

请注意,你只需要在生产构建时用到它。你不需要在开发环境应用这些插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

Rollup

为了最高效的 Rollup 生产构建,需要安装一些插件:

  1. # 如果你使用 npm
  2. npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser
  3. # 如果你使用 Yarn
  4. yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser

为了创建生产构建,确保你添加了以下插件 (顺序很重要)

  • replace 插件确保环境被正确设置。
  • commonjs 插件用于支持 CommonJS。
  • terser 插件用于压缩并生成最终的产物。
  1. plugins: [
  2. // ...
  3. require('rollup-plugin-replace')({
  4. 'process.env.NODE_ENV': JSON.stringify('production')
  5. }),
  6. require('rollup-plugin-commonjs')(),
  7. require('rollup-plugin-terser')(),
  8. // ...
  9. ]

点击查看完整的安装示例。

请注意,你只需要在生产构建时用到它。你不需要在开发中使用 terser 插件或者 replace 插件替换 'production' 变量,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

webpack

注意:

如果你使用了 Create React App,请跟随上面的说明进行操作。只有当你直接配置了 webpack 才需要参考以下内容。

在生产模式下,Webpack v4+ 将默认对代码进行压缩:

  1. const TerserPlugin = require('terser-webpack-plugin');
  2. module.exports = {
  3. mode: 'production'
  4. optimization: {
  5. minimizer: [new TerserPlugin({ /* additional options here */ })],
  6. },
  7. };

你可以在 webpack 文档中了解更多内容。

请注意,你只需要在生产构建时用到它。你不需要在开发中使用 TerserPlugin 插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。

使用 Chrome Performance 标签分析组件

开发模式下,你可以通过支持的浏览器可视化地了解组件是如何 挂载、更新以及卸载的。例如:

在 Chrome 时间线中的 React 组件
在 Chrome 中进行如下操作:

  • 临时禁用所有的 Chrome 扩展,尤其是 React 开发者工具。他们会严重干扰度量结果!

  • 确保你是在 React 的开发模式下运行应用。

  • 打开 Chrome 开发者工具的 Performance 标签并按下 Record

  • 对你想分析的行为进行复现。尽量在 20 秒内完成以避免 Chrome 卡住。

  • 停止记录。

  • User Timing 标签下会显示 React 归类好的事件。

你可以查阅这篇文章以获取更详尽的指导。

需要注意的是在生产环境中组件会相对渲染得更快些。当然了,这能帮助你查看是否有不相关的组件被错误地更新,以及 UI 更新的深度和频率。

目前只有 Chrome、Edge 和 IE 支持该功能,但是我们使用的是标准的用户计时 API。我们期待有更多浏览器能支持它。

使用开发者工具中的分析器对组件进行分析

react-dom 16.5+ 和 react-native 0.57+ 加强了分析能力。在开发模式下,React 开发者工具会出现分析器标签。你可以在《介绍 React 分析器》这篇博客中了解概述。你也可以在 YouTube 上观看分析器的视频指导。

如果你还未安装 React 开发者工具,你可以在这里找到它们:

注意

react-dom 的生产分析包也可以在 react-dom/profiling 中找到。通过查阅 fb.me/react-profiling 来了解更多关于使用这个包的内容。

虚拟化长列表

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

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

避免调停

React 构建并维护了一套内部的 UI 渲染描述。它包含了来自你的组件返回的 React 元素。该描述使得 React 避免创建 DOM 节点以及没有必要的节点访问,因为 DOM 操作相对于 JavaScript 对象操作更慢。虽然有时候它被称为“虚拟 DOM”,但是它在 React Native 中拥有相同的工作原理。

当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。

你可以通过 React 开发者工具可视化地查看这些重新渲染的虚拟 DOM:

考虑这种情况:

React 开发者工具更新高亮示例
注意到当我们输入第二个待办事项时,第一个待办事项在每次按键时也一并闪烁了。这意味着输入时,它也被 React 一并重新渲染了。这通常被称作“无用的”渲染。我们知道这是毫无必要的,因为第一个待办事项并没有改变,但是 React 并不知道。

即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。该方法会在重新渲染前被触发。其默认实现总是返回 true,让 React 执行更新:

  1. shouldComponentUpdate(nextProps, nextState) {
  2. return true;
  3. }

如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。

在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现。

shouldComponentUpdate 的作用

这是一个组件的子树。每个节点中,SCU 代表 shouldComponentUpdate 返回的值,而 vDOMEq 代表返回的 React 元素是否相同。最后,圆圈的颜色代表了该组件是否需要被调停。

should component update

节点 C2 的 shouldComponentUpdate 返回了 false,React 因而不会去渲染 C2,也因此 C4 和 C5 的 shouldComponentUpdate 不会被调用到。

对于 C1 和 C3,shouldComponentUpdate 返回了 true,所以 React 需要继续向下查询子节点。这里 C6 的 shouldComponentUpdate 返回了 true,同时由于渲染的元素与之前的不同使得 React 更新了该 DOM。

最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。

显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate 使得 render 并没有被调用。因此它们也不需要对比元素了。

示例

如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:

  1. class CounterButton extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {count: 1};
  5. }
  6. shouldComponentUpdate(nextProps, nextState) {
  7. if (this.props.color !== nextProps.color) {
  8. return true;
  9. }
  10. if (this.state.count !== nextState.count) {
  11. return true;
  12. }
  13. return false;
  14. }
  15. render() {
  16. return (
  17. <button
  18. color={this.props.color}
  19. onClick={() => this.setState(state => ({count: state.count + 1}))}>
  20. Count: {this.state.count}
  21. </button>
  22. );
  23. }
  24. }

在这段代码中,shouldComponentUpdate 仅检查了 props.colorstate.count 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 propsstate 中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent 就行了。所以这段代码可以改成以下这种更简洁的形式:

  1. class CounterButton extends React.PureComponent {
  2. constructor(props) {
  3. super(props);
  4. this.state = {count: 1};
  5. }
  6. render() {
  7. return (
  8. <button
  9. color={this.props.color}
  10. onClick={() => this.setState(state => ({count: state.count + 1}))}>
  11. Count: {this.state.count}
  12. </button>
  13. );
  14. }
  15. }

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。例如,你想要一个 ListOfWords 组件来渲染一组用逗号分开的单词。它有一个叫做 WordAdder 的父组件,该组件允许你点击一个按钮来添加一个单词到列表中。以下代码并不正确:

  1. class ListOfWords extends React.PureComponent {
  2. render() {
  3. return <div>{this.props.words.join(',')}</div>;
  4. }
  5. }
  6. class WordAdder extends React.Component {
  7. constructor(props) {
  8. super(props);
  9. this.state = {
  10. words: ['marklar']
  11. };
  12. this.handleClick = this.handleClick.bind(this);
  13. }
  14. handleClick() {
  15. // 这部分代码很糟,而且还有 bug
  16. const words = this.state.words;
  17. words.push('marklar');
  18. this.setState({words: words});
  19. }
  20. render() {
  21. return (
  22. <div>
  23. <button onClick={this.handleClick} />
  24. <ListOfWords words={this.state.words} />
  25. </div>
  26. );
  27. }
  28. }

问题在于 PureComponent 仅仅会对新老 this.props.words 的值进行简单的对比。由于代码中 WordAdderhandleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。

不可变数据的力量

避免该问题最简单的方式是避免更改你正用于 props 或 state 的值。例如,上面 handleClick 方法可以用 concat 重写:

  1. handleClick() {
  2. this.setState(state => ({
  3. words: state.words.concat(['marklar'])
  4. }));
  5. }

ES6 数组支持扩展运算符,这让代码写起来更方便了。如果你在使用 Create React App,该语法已经默认支持了。

  1. handleClick() {
  2. this.setState(state => ({
  3. words: [...state.words, 'marklar'],
  4. }));
  5. };

你可以用类似的方式改写代码来避免可变对象的产生。例如,我们有一个叫做 colormap 的对象。我们希望写一个方法来将 colormap.right 设置为 'blue'。我们可以这么写:

  1. function updateColorMap(colormap) {
  2. colormap.right = 'blue';
  3. }

为了不改变原本的对象,我们可以使用 Object.assign 方法:

  1. function updateColorMap(colormap) {
  2. return Object.assign({}, colormap, {right: 'blue'});
  3. }

现在 updateColorMap 返回了一个新的对象,而不是修改老对象。Object.assign 是 ES6 的方法,需要 polyfill。

这里有一个 JavaScript 的提案,旨在添加对象扩展属性以使得更新不可变对象变得更方便:

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

如果你在使用 Create React App,Object.assign 以及对象扩展运算符已经默认支持了。

使用不可变数据结构

Immutable.js 是另一种解决方案。它通过结构共享提供了不可变、持久化集合:

  • 不可变:一旦创建,一个集合便不能再被修改。
  • 持久化:对集合进行修改,会创建一个新的集合。之前的集合仍然有效。
  • 结构共享:新的集合会尽可能复用之前集合的结构,以最小化拷贝操作来提高性能。不可变数据使得追踪变更非常容易。每次变更都会生成一个新的对象使得我们只需要检查对象的引用是否改变。举个例子,这是一段很常见的 JavaScript 代码:
  1. const x = { foo: 'bar' };
  2. const y = x;
  3. y.foo = 'baz';
  4. x === y; // true

由于 y 被指向和 x 相同的对象,虽然我们修改了 y,但是对比结果还是 true。你可以使用 immutable.js 来写相似的代码:

  1. const SomeRecord = Immutable.Record({ foo: null });
  2. const x = new SomeRecord({ foo: 'bar' });
  3. const y = x.set('foo', 'baz');
  4. const z = x.set('foo', 'bar');
  5. x === y; // false
  6. x === z; // true

在这个例子中,修改 x 后我们得到了一个新的引用,我们可以通过判断引用 (x === y) 来验证 y 中存的值和原本 x 中存的值不同。

还有其他可以帮助实现不可变数据的库,分别是 Immerimmutability-helper 以及 seamless-immutable

不可变数据结构使你可以方便地追踪对象的变化,这是应用 shouldComponentUpdate 所需要的。让性能得以提升。