优化性能
在内部,React使用几种巧妙的技术来最大限度地减少更新 UI 所需的昂贵的 DOM 操作的数量。 对于大多数应用,使用 React 可以达到一个快速的用户界面,而不需要做太多的工作来专门优化性能。 然而,有几种方法可以加快你的 React 应用。
使用生产版本
如果你在你的 React 应用程序中进行检测性能问题时,确保你正在使用压缩过的生产版本。
默认情况下,React包含很多在开发过程中很有帮助的警告。然而,这会导致 React 更大更慢。因此,在部署应用时,请确认使用了生产版本。
如果你不确定构建过程是否正确,可以在 chrome 中安装 React开发者工具 。当你访问一个生产模式的React页面时,这个工具的图标会有一个黑色的背景:
如果你访问一个开发模式的 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
为了构建生产版本,务必添加这些设置指令 (这点很重要):
举个例子:
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
为了构建生产版本,务必添加这些插件 (这点很重要):
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) 的各个过程。例如:
在 Chrome 中操作如下:
-
通过添加
?react_perf
查询字段加载你的应用(例如:http://localhost:3000/?react_perf
)。 -
打开 Chrome DevTools Performance 并点击 Record 。( 愚人码头注:如何使用时间轴工具 译文)
-
执行你想要分析的操作,不要超过20秒,否则 Chrome 可能会挂起。
-
停止记录。
-
在 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 (高亮显示更新) 选项:
与你的页面进行交互,你应该会看到,所有重新渲染的组件周围都会出现高亮显示的边框。 反过来,这可以让你知道没有必要重新渲染的组件。 你可以查看 Ben Edelstein 的博客文章 了解更多关于 React DevTools 功能的信息。
考虑这个例子:
请注意,当我们进入第二个待办事项时,每次输入时,第一个待办事项也会在屏幕上闪烁。 这意味着它正在被重新渲染。 这有时被称为 “浪费” 的渲染。 我们知道这是没有必要的,因为第一个待办事项的内容没有改变,但是 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 元素是否相等。最后,圆圈内的颜色代表组件是否需要更新。
因为以 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.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 提供了一个 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-immutable 和 immutability-helper。
不可变数据提供了一种更简单的方式来追踪对象的改变,这正是我们实现 shouldComponentUpdate
所需要的。这将会提供可观的性能提升。