优化性能

在内部,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. 通过添加 ?react_perf 查询字段加载你的应用(例如:http://localhost:3000/?react_perf)。

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

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

  4. 停止记录。

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

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

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

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

虚拟化长列表

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

React 虚拟化 是一个流行的 “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(prevState => ({
    words: prevState.words.concat(['marklar'])
  }));
}

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

handleClick() {
  this.setState(prevState => ({
    words: [...prevState.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 所需要的。这将会提供可观的性能提升。