优化性能

在内部,React使用几种巧妙的技术来最大限度地减少更新 UI 所需的昂贵的 DOM 操作的数量。 对于大多数应用,使用 React 可以达到一个快速的用户界面,而不需要做太多的工作来专门优化性能。 然而,有几种方法可以加快你的 React 应用。

使用生产版本

如果你在你的 React 应用程序中进行检测性能问题时,确保你正在使用压缩过的生产版本。

默认情况下,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 创建的,运行:

npm run build

这将会在该项目的 build/ 文件夹内创建一个生产版本的应用。

注意只有发布项目时才有必要这样做,正常开发时,使用 npm start

单文件构建

我们提供压缩好的生产版本的 React 和 React DOM 文件:

<script src="https://unpkg.com/react@15/dist/react.min.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>

注意只有结尾为 .min.js 的React文件才是适合生产使用的。

Brunch

对于创建最高效的 Brunch 生产版本,需要安装 uglify-js-brunch 插件:

# 如果使用 npm
npm install --save-dev uglify-js-brunch

# 如果使用 Yarn
yarn add --dev uglify-js-brunch

然后,为了创建生产构建版本,在 build 命令后添加 -p 参数:

brunch build -p

注意只有生产版本需要这样操作。不要在开发环境中安装这个插件或者使用 -p 参数,因为它会隐藏掉有用的 React 警告并使构建过程更慢。

Browserify

为了创建最高效的 Browserify 生产版本,需要安装一些插件:

# 如果使用 npm
npm install --save-dev bundle-collapser envify uglify-js uglifyify 

# 如果使用 Yarn
yarn add --dev bundle-collapser envify uglify-js uglifyify 

为了构建生产版本,务必添加这些设置指令 (这点很重要)

  • envify 该插件确保正确的编译环境,全局安装(-g)。
  • uglifyify 该插件移除了开发接口。全局安装(-g)。
  • 最后,以上结果都被输添加至 uglify-js 来得到整合。(了解原因)。

举个例子:

browserify ./index.js \
  -g [ envify --NODE_ENV production ] \
  -g uglifyify \
  -p bundle-collapser/plugin \
  | uglifyjs --compress --mangle > ./bundle.js

注意:

包的名称是 uglify-js ,但是它提供的文件叫 uglifyjs
这不是一个错字。

注意只有生产版本需要这样操作。不要在开发环境中安装这些插件,因为它们会隐藏掉有用的 React 警告并使构建过程更慢。

Rollup

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

# 如果使用 npm
npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify 

# 如果使用 Yarn
yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify 

为了构建生产版本,务必添加这些插件 (这点很重要):

  • replace 该插件确保正确的编译环境。
  • commonjs 该插件在 Rollup 内提供对 CommonJS 的支持。
  • uglify 该插件压缩生成最终版本。
plugins: [
  // ...
  require('rollup-plugin-replace')({
    'process.env.NODE_ENV': JSON.stringify('production')
  }),
  require('rollup-plugin-commonjs')(),
  require('rollup-plugin-uglify')(),
  // ...
]

一个完整的安装例子 查看这个 gist.

注意只有生产版本需要这样操作。你不应该在开发环境中应用 uglify 插件 和 replace 插件的 'production' 值,因为它们会隐藏掉有用的 React 警告并使构建过程更慢。

webpack

注意:

如果你正在使用 Create React App 方式,参考上述文档
本节只适用于直接配置Webpack的情况。

为了创建最高效的Webpack生产版本,需要在生产版本的配置中添加这些插件:

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

了解更多参见 Webpack文档

注意只有生产版本需要这样操作。你不应该在开发环境中应用 UglifyJsPlugin 插件 和 DefinePlugin 插件的 'production' 值,因为它们会隐藏掉有用的 React 警告并使构建过程更慢。

使用 Chrome 性能分析工具 分析组件性能

开发模式 中,你可以在支持相关功能的浏览器中使用性能工具来可视化组件 装载(mount) ,更新(update) 和 卸载(unmount) 的各个过程。例如:

React components in Chrome timeline

在 Chrome 中操作如下:

  1. 暂时 停用所有 Chrome 扩展程序 ,尤其是React DevTools 。 它们可以显著地影响,甚至是扭曲结果!

  2. 确保您在开发模式下运行应用程序。

  3. 打开 Chrome DevTools Performance 并点击 Record 。( 愚人码头注:如何使用时间轴工具 译文)

  4. 执行你想要分析的操作,不要超过20秒,否则 Chrome 可能会挂起。

  5. 停止记录。

  6. User Timing 标签下,React事件将会分组列出。

有关更详细的演练,请查看 Ben Schwarz 的这篇文章.

注意,上述数字是相对的,组件会在生产环境中会更快。然而,这对你分析由于错误导致不相关的组件的更新、分析组件更新的深度和频率很有帮助。

目前 Chrome ,Edge 和 IE 支持该特性,但是我们使用了标准的 User Timing API ,因此我们期待将来会有更多的浏览器支持。

使用 DevTools Profiler 分析组件

react-dom 16.5+ 和 react-native 0.57+ 通过 React DevTools Profiler 在开发模式下提供了增强的性能分析功能。在“介绍 React Profiler” 的博客文章中可以找到对该分析器的概述。该剖析器的视频演练也可以在 YouTube上找到

如果您还没有安装 React DevTools,可以在这里找到它们:

注意

react-dom 的生产分析包也可用作 react-dom/profiling 。 在 fb.me/react-profiling 中阅读有关如何使用此捆绑包的更多信息

虚拟化长列表

如果您的应用程序渲染很长的数据列表(数百或数千行),我们推荐使用称为 “windowing” (开窗口) 技术。这种技术在任何给定的时间只渲染一小部分的行,并且可以显著减少重新渲染组件的时间以及创建的DOM节点的数量。

react-windowreact-virtualized 是流行的 “windowing” (开窗口) 库。它们提供了多个可重用组件,用于显示列表,网格和表格化数据。如果你想要更适合你的应用程序的特定用例,您还可以创建自己的 windowing 组件,如 Twitter did

避免重新渲染

React 构建并维护渲染 UI 的内部表示。它包括你从组件中返回的 React 元素。这些内部状态使得 React 只有在必要的情况下才会创建DOM节点和访问存在DOM节点,因为对 JavaScript 对象的操作是比 DOM 操作更快。这被称为”虚拟DOM”,React Native 也是基于上述原理。

当组件的 props 和 state 改变时,React 通过比较新返回的元素 和 之前渲染的元素 来决定是否有必要更新DOM元素。当二者不相等时,则更新 DOM 元素。

现在你可以使用 React DevTools 可视化这些重新渲染的虚拟DOM:

在开发者工具的控制台中,选择 React 选项卡中的 Highlight Updates (高亮显示更新) 选项:

How to enable highlight updates

与你的页面进行交互,你应该会看到,所有重新渲染的组件周围都会出现高亮显示的边框。 反过来,这可以让你知道没有必要重新渲染的组件。 你可以查看 Ben Edelstein博客文章 了解更多关于 React DevTools 功能的信息。

考虑这个例子:

React DevTools Highlight Updates example

请注意,当我们进入第二个待办事项时,每次输入时,第一个待办事项也会在屏幕上闪烁。 这意味着它正在被重新渲染。 这有时被称为 “浪费” 的渲染。 我们知道这是没有必要的,因为第一个待办事项的内容没有改变,但是 React 并不知道这一点。

即使 React 只更新更改的 DOM 节点,重新渲染仍然需要一些时间。 在许多情况下,这不是大问题,但是降低交互性能是显而易见的,你可以通过重写生命周期函数 shouldComponentUpdate 来优化性能,这是在重新渲染过程开始之前触发的。该函数的默认实现中返回的是 true,使得 React 执行更新操作:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

如果你知道在某些情况下你的组件不需要更新,那么你可以在 shouldComponentUpdate 返回 false 来跳过整个渲染过程,包括在这个组件和后面调用的 render()

在大多数情况下,您可以不用手写 shouldComponentUpdate() ,而是从 React.PureComponent 继承。 这相当于用当前和以前 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 来讲,通过比较渲染元素被剔除,对于 C2 子树和 C7 ,因为shouldComponentUpdate 被剔除,甚至都不需要比较 React 元素,也不会调用 render 方法。

例子

如果你想要你的组件仅当 props.colorstate.count 发生改变时需要更新,你可以通过 shouldComponentUpdate 函数来检查:

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

  shouldComponentUpdate(nextProps, nextState) {
    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 是否发生改变。如果这些值没有发生变化,则组件不会进行更新。如果你的组件更复杂,你可以使用类似于对 propsstate 的所有属性进行”浅比较”这种模式来决定组件是否需要更新。这种模式非常普遍,因此 React 提供了一个 helper 实现上面的逻辑:继承 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 。但 React.PureComponent 仅会进项浅比较,因此如果 props 或者 state 可能会导致浅比较失败的情况下就不能使用 React.PureComponent

如果 props 和 state 属性存在更复杂的数据结构,这可能是一个问题。例如,我们编写一个 ListOfWords 组件展现一个以逗号分隔的单词列表,在父组件 WordAdder ,当你点击一个按钮时会给列表添加一个单词。下面的代码是不能正确地工作:

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 与新的 this.props.words 之间进行前比较。因此在 WordAdder 组件中 handleClick 的代码会突变 words 数组。虽然数组中实际的值发生了变化,但旧的 this.props.words 和新的 this.props.words 值是相同的,即使 ListOfWords 需要渲染新的值,但是还是不会进行更新。

不可变数据的力量

避免这类问题最简单的方法是不要突变(mutate) props 或 state 的值。例如,上述 handleClick 方法可以通过使用 concat 重写:

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

ES6 对于数组支持展开语法 ,使得解决上述问题更加简单。如果你使用的是Create React App,默认支持该语法。

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

你可以以一种简单的方式重写上述代码,使得改变对象的同时不会突变对象,例如,如果有一个 colormap 的对象并且编写一个函数将 colormap.right 的值改为 'blue'

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

在不突变原来的对象的条件下实现上面的要求,我们可以使用Object.assign 方法:

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

updateColorMap 现在返回一个新的对象,而不是修改原来的对象。Object.assign 属于ES6语法,需要 polyfill。

JavaScript提案添加了对象展开符 ,能够更简单地更新对象而不突变对象。

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

如果你使用的是 Create React App ,Object.assign 和对象展开符默认都是可用的。

使用 Immutable 数据结构

Immutable.js 是解决上述问题的另外一个方法,其提供了通过结构共享实现(Structural Sharing)地不可变的(Immutable)、持久的(Persistent)集合:

  • 不可变(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');
const z = x.set('foo', 'bar');
x === y; // false
x === z; // true

在这种情况下,因为当改变 x 时返回新的引用,我们可以使用一个相等检查(x===y)来验证存储在y中的新值是否与存储在x中的原始值不同。

其他两个可以帮助我们使用不可变数据的库分别是:seamless-immutableimmutability-helper

不可变数据提供了一种更简单的方式来追踪对象的改变,这正是我们实现 shouldComponentUpdate 所需要的。这将会提供可观的性能提升。