Optimizing Performance

React内部使用了一些技术来最大限度的减少更新UI导致的DOM的操作数量。对于大多数应用来说,使用React不需要刻意去做专门的性能优化就能实现快速的用户界面。当然,也有一些方案可以加快你的React应用。

使用生产环境构建

如果你的React在测试过程中发现性能等问题,确保你使用的测试环境是最小的生产环境。

默认情况下,React包含许多有用的警告信息,这些警告在开发的时候非常有用,然而,也使得React更大更慢。因此在部署React应用的时候,应当使用生产版本。

如果你不确定构建应用的过程配置的是开发模式还是生产模式,你可以通过安装Chrome的React Developer Tools 来调试。如果是在生产环境访问一个React的网站,该扩展图片的背景是深色的:

bg1

如果访问的网站是开发模式下,扩展的图片背景是红色的:

bg2

一般来讲,在开发过程中会使用开发模式,而在部署过程会使用生产模式。

你可以在下面找到构建你的应用程序的文档说明。

Create React App

如果你的项目使用 Create React App 构建,通过下面命令启动:

  1. npm run build

上面命令会在你的项目的build/文件夹下构建一个生产版本。

请记住,只有在部署到生产之前才需要这样做。对于正常开发来说,使用npm start就足够了。

单文件部署(Single-Files Builds)

我们提供生产模式下的 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是适合生产环境的。

Brunch

如果使用Brunch构建高效的生产版本,安装uglify-js-brunch插件

  1. # If you use npm
  2. npm install --save-dev uglify-js-brunch
  3. # If you use Yarn
  4. yarn add --dev uglify-js-brunch

然后,如果要构建生产版本,在build命令上使用 -p 即可:

  1. brunch build -p

请记住,只需要在构建生产环境版本的时候使用该命令。在开发中不应当使用这个插件(uglify-js-brunch)也不应当使用-p参数,因为不仅仅会隐藏有用的React警告,构建速度也会更慢。

Browserify

如果使用Browserify构建生产环境版本,需要安装一下插件:

  1. # If you use npm
  2. npm install --save-dev envify uglify-js uglifyify
  3. # If you use Yarn
  4. yarn add --dev envify uglify-js uglifyify

如果要构建生产环境版本,确保安装一下午的transform(下面几个很重要):

  • envify transform 能够确保构建正确的生产环境版本。需全局安装(-g)

  • uglifyify 用来移除一些生产环境的依赖。同样全局安装(-g)

  • 最终,得到的打包文件通过管道传递给uglify-js进行处理.(了解为什么)

例如:

  1. browserify ./index.js \
  2. -g [ envify --NODE_ENV production ] \
  3. -g uglifyify \
  4. | uglifyjs --compress --mangle > ./bundle.js

注意: 包的名字是uglify-js,但是提供的名称是uglifyjs.
这不是拼错了

同样请记住,只需要在生产环境中使用即可。在开发环境中,不应当使用这些插件,因为会隐藏有用的警告信息,而且构建速度也非常慢。

Rollup

如果使用 Rollup 构建生产环境版本,需要安装下面这些插件:

  1. # If you use npm
  2. npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify
  3. # If you use Yarn
  4. yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify

如果要构建生产环境版本,确保安装下这些插件(非常重要):

  • replace 确保配置了正确的构建环境

  • commonjs 插件提供了对 Rollup 的 CommonJS 支持

  • uglify 插件 最终压缩和管理构建的包。

  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-uglify')(),
  8. // ...
  9. ]

完整的配置示例可以查看简介

同样请注意,只需要在构建生产环境版本的时候使用上述方式。在开发环境下,不要使用uglify插件或者是replace插件的时候使用production,因为会隐藏有用的React警告,而且构建速度更慢。

webpack

注意: 如果你使用 Create Raect App ,请参照上面的介绍 the instructions above
如果你直接使用webpack进行构建的话,下面章节会有帮助。

如果使用webpack构建生产环境版本,确保在配置中包含如下插件:

  1. new webpack.DefinePlugin({
  2. 'process.env': {
  3. NODE_ENV: JSON.stringify('production')
  4. }
  5. }),
  6. new webpack.optimize.UglifyJsPlugin()

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

请记住,仅在生产环境中使用上述插件。开发环境中不应该使用UglifyJsPlugin或者是DefinePlugin,因为会隐藏有用的React警告信息,构建速度也更慢。

使用Chrome性能分析标签来分析React组件

开发模式下,你可以使用支持的浏览器中的性能工具来可视化组件的安装、更新和卸载。如:

bg3

如果在Chrome中使用:

  1. 使用一个查询字符串?react_perf来加载应用程序。(如:http://localhost:3000/?react_perf)

  2. 打开Chrome开发者工具的Performance(性能)选项卡然后开始Record(记录)

  3. 执行你想要分析的操作。不要超过20s因为Chrome可能会挂起

  4. 停止记录(Stop Recording)

  5. React 事件被分在 User Timing 标签下

请注意,这些指示标准(数字)是相对的(开发环境下),组件在生产环境下将呈现更快的速度。不过,当不相关的UI更新出现错误的时候或者是发现更新的深度和频率等方面,它能够帮助你尽快的发现问题。

目前 Chrome 、 Edge 和 IE 支持这些特性,不过我们使用标准的User Timing API ,因此我们希望能够有更多的浏览器支持这些特性。


Avoid Reconciliation

React在内部构建和维护一系列UI,包含了组件返回的React元素。这种方式可以使用React避免创建DOM节点并且没必要访问已经存在的DOM节点,所以可能会比直接操作JavaScript对象慢一些。有些时候,称其为“虚拟DOM”,不过和React Native的工作方式一样。

当一个组件的 props 或者是 state 改变的时候,React比较新的元素与之前渲染的元素,从而决定是否需要返回更新后的DOM。如果他们不相等,React将返回新DOM。

在某些情况下,你的组件可以通过重写生命周期函数shouldComponentUpdate来进行加速,因为在重新渲染过程开始之前便会被触发。

这个函数默认返回true,表示Raect需要进行更新:

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

如果在某些情况下吗,你能够明确你的组件不需要进行更新,你可以从shouldComponentUpdate函数返回false,跳过重新渲染的过程,无论在该组件上调用render()还是之后调用render()都可以实现。


Action中的 shouldComponentUpdate

下面是一个组件树。对于每一个组件,SCU表示shouldComponentUpdate返回什么值,而vDOMq表示渲染的React元素是否是等价的。最后,圆圈的颜色表示,组件是否需要进行重绘。

chonghui

因为C2shouldComponentUpdate返回false,所以React没有尝试去重新渲染C2,并且也没有在C4C5上触发shouldComponentUpdate.

对于C1C3来说,shouldComponentUpdate返回true,所以React需要继续往下遍历虚拟DOM树,并且去检查他们。对于C6来说,shouldComponentUpdate返回true,并且由于渲染的元素与已经存在的元素不相等,所以React要更新DOM。

C8 的情况比较有趣,React会渲染该组件,但是由于其返回的React元素和之前渲染的元素等价,所以不会更新DOM。

请注意,上述情况中,React只需要对C6改变DOM,这是一定会发生的。然而对于C8来说,通过和已经渲染的元素进行比较,决定不改变DOM,而对于C2的子树和C7来说,它不需要和已经渲染的元素进行比较,甚至不会去调用shouldComponentUpdate,也不会调用render()方法。


例子:

如果你改变下面组件的唯一方式是通过props.colorstate.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. // This section is bad style and causes a 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会对is.props.words的新值和旧值之间进行一个浅比较,虽然WordAdderhandleClick方法会发生变化,但是这种变化实际上在进行this.props.words的比较的时候,即使数组中的word实际上已经改变,但是旧值和新值也是相等的。所以ListOfWords不会发生更新,即使它应当呈现新单词。


不突变数据

避免上述问题最简单的方法是不要使用 props 或者 state 操作变异值。比如上面handleClick方法可以使用concat进行重写。

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

ES6提供了数组的分解运算符来方便操作,如果你使用Create React App,下面代码默认就是起作用的:

  1. handleClick() {
  2. this.setState(prevState => ({
  3. words: [...prevState.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的API,需要一个polyfill。

JavaScript的提案(ES7)中有一个更好的对象分解运算符来更方便的编写:

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

如果你是使用 Create React App,并且 Object.assign和对象分解运算符都默认起作用。

使用不可变的数据结构

Immutable.js是另一种解决此问题的方案。它通过结构共享提供不变的、持久的数据集合:

  • Immutable(不可变性) : 一旦创建,数据集合不能再另一个时间点更改

  • Persistent (一致性): 可以从先前的集合和已经创建的集合中返回一个新的集合,创建新的集合后,原来的数据集合仍然有效。

  • Structural Sharing (结构共享):应当尽可能通过原始集合的结构创建新的集合,从而减少结构复制提高性能。

不可变数据使得追踪数据变化变的容易。新的改动总是会返回一个新的对象,因此我们只需要检查对象的引用是否已经更改,例如,下面普通的JavaScript代码中:

  1. const x = { foo: 'bar' };
  2. const y = x;
  3. y.foo = 'baz';
  4. x === y; // true

虽然 y 已经更改过了,但是它还是和 X 完全相等,你可以通过 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 中的原始值不同。

另外还有两个库能够帮助我们来使用不可变的数据集合是:seamless-immutableimmutable-helper .

Immutable数据结构给你提供了一种追踪对象变化的更方便的方式,这也是我们需要实现的应用程序的shouldComponentUpdate。这可以为你提供良好的性能提升。