Skip to content

Latest commit

 

History

History
435 lines (303 loc) · 20.2 KB

optimizing-performance.md

File metadata and controls

435 lines (303 loc) · 20.2 KB
id title permalink redirect_from
optimizing-performance
Optimizing Performance
docs/optimizing-performance.html
docs/advanced-performance.html

UI 更新需要对 DOM 进行操作,而 React 内部通过几种巧妙的技术以最小化操作 DOM 来降低性能损耗。对于大部分应用而言,使用 React 不需要特别去优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。

使用生产版本 {#use-the-production-build}

当你需要对你的 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}

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

npm run build

这段脚本将在你的项目下的 build/ 目录中生成生产版本。

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

单文件构建 {#single-file-builds}

我们提供了可以在生产环境使用的 React 和 React 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 {#brunch}

通过安装 uglify-js-brunch 插件,来获得最高效的 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 envify uglify-js uglifyify 

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

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

  • envify 转换器用于设置正确的环境变量。设置为全局 (-g)。
  • uglifyify 转换器移除开发相关的引用代码。同样设置为全局 (-g)。
  • 最后,将产物传给 uglify-js 用以压缩(为什么要这么做?)。

举例:

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

注意:

虽然这个包名叫做 uglify-js,但是执行文件叫做 uglifyjs
这不是拼写错误。

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

Rollup {#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 插件用于支持 CommonJS。
  • uglify 插件用于压缩并生成最终的产物。
plugins: [
  // ...
  require('rollup-plugin-replace')({
    'process.env.NODE_ENV': JSON.stringify('production')
  }),
  require('rollup-plugin-commonjs')(),
  require('rollup-plugin-uglify')(),
  // ...
]

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

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

webpack {#webpack}

Note:

如果你使用了 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 性能标签分析组件 {#profiling-components-with-the-chrome-performance-tab}

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

在 Chrome 时间线中的 React 组件

在 Chrome 如下操作:

  1. 临时禁用所有的 Chrome 扩展,尤其是 React 开发工具。它们会干扰度量结果!

  2. 确保你是在开发模式下运行应用。

  3. 打开 Chrome 开发工具的 Performance 标签并按下 Record

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

  5. 停止记录。

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

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

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

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

使用开发工具的分析器对组件进行分析 {#profiling-components-with-the-devtools-profiler}

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

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

注意

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

虚拟化长列表 {#virtualize-long-lists}

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

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

避免调停 {#avoid-reconciliation}

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

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

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

在开发者控制台的 React 标签勾选 Highlight Updates

如何开启更新高亮

当与你的页面进行交互时,你会看到被重新渲染的组件立刻出现了彩色的边框。这能帮助你找到那些没有必要的重新渲染。你可以在 Ben Edelstein这篇博客中学到更多关于 React 开发工具的功能。

考虑这种情况:

React 开发工具更新高亮示例

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

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

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

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

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

shouldComponentUpdate 的作用 {#shouldcomponentupdate-in-action}

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

节点 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 并没有被调用。因此它们也不需要对比元素了。

示例 {#examples}

如果你的组件只有当 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.colorstate.count 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 propsstate 中所有的字段,以此来决定是否组件需要更新。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 的值进行简单的对比。由于代码中 WordAdderhandleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。

不可变数据的力量 {#the-power-of-not-mutating-data}

避免该问题最简单的方式是避免更改你正用于 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 以及对象扩展运算符已经默认支持了。

使用不可变数据结构 {#using-immutable-data-structures}

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

  • 不可变:一旦创建,一个集合便不能再被修改。
  • 持久化:对集合进行修改,会创建一个新的集合。之前的集合仍然有效。
  • 结构共享:新的集合会尽可能复用之前集合的结构,以最小化拷贝操作来提高性能。

不可变数据使得追踪变更非常容易。每次变更都会生成一个新的对象使得我们只需要检查对象的引用是否改变。举个例子,这是一段很常见的 JavaScript 代码:

const x = { foo: 'bar' };
const y = x;
y.foo = 'baz';
x === y; // true

由于 y 被指向和 x 相同的对象,虽然我们修改了 y,但是对比结果还是 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 所需要的。让性能得以提升。