Skip to content

Optimizing Performance

性能优化

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

使用生产版本

当你需要对你的React应用进行benchmark,或者遇到了性能问题,请确保你正在使用压缩后的生产版本。 React默认包含许多有用的警告信息。这些警告信息需要在开发过程中非常有帮助。然而这使得React变得更大且更慢,所以你需要确保部署时使用了生产版本。 如果你不能确定你的编译过程是否正确,你可以用过React开发者工具来检查。如果你浏览一个基于React生产版本的网站,图标会变成深色,如果是基于开发模式的网站,图标背景会变成红色。

Create React App

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

npm run build

这段命令将在你的项目下的build/目录下生产对应的生产版本。 注意只有在生产部署前才需要执行这个命令。正常开发使用npm start既可。

单文件构建

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

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

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

Brunch

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

# 如果你使用npm
npm install --save-dev terser-brunch

# 如果你使用Yarn
yarn add --dev terser-brunch

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

brunch build -p

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

Browserify

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

# 如果你使用npm
npm install --save-dev envify terser uglifyify

# 如果你使用了Yarn
yarn add --dev envify terser uglifyify

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

  • envify转化器用于设置正确的变量。设置为全局-g
  • uglifyify转换器移除开发相关的引用代码。同样设置为全局-g
  • 最后,将产物传给terser进行压缩。 举个例子:
browserify ./index.js -g [ envify --NODE_ENV production ] -g uglifyify | terser --compress --mangle > ./bundle.js

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

Rollup

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

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

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

为了创建生产构建,确保添加了以下插件:

  • replace插件确保环境变量被正确设置
  • commonjs插件用于支持CommonJS
  • terser插件用于压缩并生成最终的产物
plugins:[
    require('rollup-plugin-replace')({
        'process.env.NODE_ENV': JSON.stringify('production')
    }),
    require('rollup-plugin-commonjs')(),
    require('rollup-plugin-terser')(),
]

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

webpack

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

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [new TerserPlugin({ /* additional options here */ })],
  },
};

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

虚拟化长列表

如果你的应用渲染了长列表(上百甚至上千的数据),我们推荐使用"虚拟滚动技术"。这项技术在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建DOM节点的数量。 react-windowreact-virtualized是热门的虚拟滚动库。他们提供了多种可复用的组件,用于展示列表、网格和表格数据。如果你想要一些针对你的应用做定制优化,你也可以创建你自己的虚拟滚动组件。

避免调停

React构建并维护了一套内部的UI渲染描述。他包含了来自你的组件返回的React元素。该描述使得React避免创建DOM节点以及没有必要的节点访问,因为DOM操作相对于JavaScript对象操作更慢了。虽然有时间它被称为虚拟DOM,但是在React Native中有相同的工作原理。 当一个组件的props或state变更,React会将最新返回的元素与之前的渲染的元素进行对比,以此决定是有必要更新真实的DOM。当他们不相同时,React会更新该DOM。 即使React只更新改变了DOM节点,重新渲染仍然花费了一些时间。在大部分情况下他并不是问题,不过如果它已经慢到让你注意了,你可以通过覆盖生命周期方法shouldComponentUpdate来进行提速。该方法会在重新渲染前被触发。其默认实现总是返回true,让你React执行更新:

shouldComponentUpdate(nextProps,nextState){
    return true;
}

如果你知道在什么情况下你的组件不需要更新,你可以在shouldComponentUpdate中返回false来跳过整个渲染过程。其包括该组件的render调用以及之后的操作。 在大部分情况下,你可以继承React.PureComponent以代替手写shouldComponentUpdate.她用当前的props和state的浅比较覆写了shouldComponentUpdate的实现。

shouldComponentUpdate的作用

如果你的组件只有当props.color或者state.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 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似"浅比较"的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 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。但它只进行浅比较,所以当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() {
    // 这部分代码很糟,而且还有 bug
    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 的值进行简单的对比。由于代码中 WordAdder 的 handleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。

不可变数据的力量

避免该问题最简单的方法是避免更新你正在用于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']
    }))
}

你可以用类似的方式改写代码来避免可变对象的产生。实际上的做法是,不是直接在原对象上修改,而是返回一个新对象和原对象保持一致并进行此次修改。