Optimizing Performance
React内部使用了一些技术来最大限度的减少更新UI导致的DOM的操作数量。对于大多数应用来说,使用React不需要刻意去做专门的性能优化就能实现快速的用户界面。当然,也有一些方案可以加快你的React应用。
使用生产环境构建
如果你的React在测试过程中发现性能等问题,确保你使用的测试环境是最小的生产环境。
默认情况下,React包含许多有用的警告信息,这些警告在开发的时候非常有用,然而,也使得React更大更慢。因此在部署React应用的时候,应当使用生产版本。
如果你不确定构建应用的过程配置的是开发模式还是生产模式,你可以通过安装Chrome的React Developer Tools 来调试。如果是在生产环境访问一个React的网站,该扩展图片的背景是深色的:
如果访问的网站是开发模式下,扩展的图片背景是红色的:
一般来讲,在开发过程中会使用开发模式,而在部署过程会使用生产模式。
你可以在下面找到构建你的应用程序的文档说明。
Create React App
如果你的项目使用 Create React App 构建,通过下面命令启动:
npm run build
上面命令会在你的项目的build/
文件夹下构建一个生产版本。
请记住,只有在部署到生产之前才需要这样做。对于正常开发来说,使用npm start
就足够了。
单文件部署(Single-Files 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
是适合生产环境的。
Brunch
如果使用Brunch构建高效的生产版本,安装uglify-js-brunch
插件
# If you use npm
npm install --save-dev uglify-js-brunch
# If you use Yarn
yarn add --dev uglify-js-brunch
然后,如果要构建生产版本,在build
命令上使用 -p
即可:
brunch build -p
请记住,只需要在构建生产环境版本的时候使用该命令。在开发中不应当使用这个插件(uglify-js-brunch
)也不应当使用-p
参数,因为不仅仅会隐藏有用的React警告,构建速度也会更慢。
Browserify
如果使用Browserify
构建生产环境版本,需要安装一下插件:
# If you use npm
npm install --save-dev envify uglify-js uglifyify
# If you use Yarn
yarn add --dev envify uglify-js uglifyify
如果要构建生产环境版本,确保安装一下午的transform(下面几个很重要):
envify transform 能够确保构建正确的生产环境版本。需全局安装(
-g
)uglifyify 用来移除一些生产环境的依赖。同样全局安装(
-g
)
例如:
browserify ./index.js \
-g [ envify --NODE_ENV production ] \
-g uglifyify \
| uglifyjs --compress --mangle > ./bundle.js
注意: 包的名字是
uglify-js
,但是提供的名称是uglifyjs
.
这不是拼错了
同样请记住,只需要在生产环境中使用即可。在开发环境中,不应当使用这些插件,因为会隐藏有用的警告信息,而且构建速度也非常慢。
Rollup
如果使用 Rollup 构建生产环境版本,需要安装下面这些插件:
# If you use npm
npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify
# If you use Yarn
yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-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
注意: 如果你使用 Create Raect App ,请参照上面的介绍 the instructions above
如果你直接使用webpack进行构建的话,下面章节会有帮助。
如果使用webpack构建生产环境版本,确保在配置中包含如下插件:
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin()
你可以在 webpack 文档 中了解更多。
请记住,仅在生产环境中使用上述插件。开发环境中不应该使用UglifyJsPlugin
或者是DefinePlugin
,因为会隐藏有用的React警告信息,构建速度也更慢。
使用Chrome性能分析标签来分析React组件
在开发模式下,你可以使用支持的浏览器中的性能工具来可视化组件的安装、更新和卸载。如:
如果在Chrome中使用:
使用一个查询字符串
?react_perf
来加载应用程序。(如:http://localhost:3000/?react_perf)打开Chrome开发者工具的
Performance
(性能)选项卡然后开始Record(记录)执行你想要分析的操作。不要超过20s因为Chrome可能会挂起
停止记录(Stop Recording)
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需要进行更新:
shouldComponentUpdate(nextProps, nextState) {
return true;
}
如果在某些情况下吗,你能够明确你的组件不需要进行更新,你可以从shouldComponentUpdate
函数返回false
,跳过重新渲染的过程,无论在该组件上调用render()
还是之后调用render()
都可以实现。
Action中的 shouldComponentUpdate
下面是一个组件树。对于每一个组件,SCU
表示shouldComponentUpdate
返回什么值,而vDOMq
表示渲染的React元素是否是等价的。最后,圆圈的颜色表示,组件是否需要进行重绘。
因为C2的shouldComponentUpdate
返回false
,所以React没有尝试去重新渲染C2,并且也没有在C4和C5上触发shouldComponentUpdate
.
对于C1和C3来说,shouldComponentUpdate
返回true
,所以React需要继续往下遍历虚拟DOM树,并且去检查他们。对于C6来说,shouldComponentUpdate
返回true
,并且由于渲染的元素与已经存在的元素不相等,所以React要更新DOM。
C8 的情况比较有趣,React会渲染该组件,但是由于其返回的React元素和之前渲染的元素等价,所以不会更新DOM。
请注意,上述情况中,React只需要对C6改变DOM,这是一定会发生的。然而对于C8来说,通过和已经渲染的元素进行比较,决定不改变DOM,而对于C2的子树和C7来说,它不需要和已经渲染的元素进行比较,甚至不会去调用shouldComponentUpdate
,也不会调用render()
方法。
例子:
如果你改变下面组件的唯一方式是通过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() {
// This section is bad style and causes a 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
会对is.props.words
的新值和旧值之间进行一个浅比较,虽然WordAdder
的handleClick
方法会发生变化,但是这种变化实际上在进行this.props.words
的比较的时候,即使数组中的word实际上已经改变,但是旧值和新值也是相等的。所以ListOfWords
不会发生更新,即使它应当呈现新单词。
不突变数据
避免上述问题最简单的方法是不要使用 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的API,需要一个polyfill。
JavaScript的提案(ES7)中有一个更好的对象分解运算符来更方便的编写:
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}
如果你是使用 Create React App,并且 Object.assign
和对象分解运算符都默认起作用。
使用不可变的数据结构
Immutable.js是另一种解决此问题的方案。它通过结构共享提供不变的、持久的数据集合:
Immutable(不可变性) : 一旦创建,数据集合不能再另一个时间点更改
Persistent (一致性): 可以从先前的集合和已经创建的集合中返回一个新的集合,创建新的集合后,原来的数据集合仍然有效。
Structural Sharing (结构共享):应当尽可能通过原始集合的结构创建新的集合,从而减少结构复制提高性能。
不可变数据使得追踪数据变化变的容易。新的改动总是会返回一个新的对象,因此我们只需要检查对象的引用是否已经更改,例如,下面普通的JavaScript代码中:
const x = { foo: 'bar' };
const y = x;
y.foo = 'baz';
x === y; // true
虽然 y
已经更改过了,但是它还是和 X
完全相等,你可以通过 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-immutable 和 immutable-helper .
Immutable数据结构给你提供了一种追踪对象变化的更方便的方式,这也是我们需要实现的应用程序的shouldComponentUpdate
。这可以为你提供良好的性能提升。