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既可。
单文件构建
我们提供了可以在生产环境使用的单文件版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
通过安装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转化器用于设置正确的变量。设置为全局-guglifyify转换器移除开发相关的引用代码。同样设置为全局-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插件用于支持CommonJSterser插件用于压缩并生成最终的产物
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-window和react-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']
}))
}你可以用类似的方式改写代码来避免可变对象的产生。实际上的做法是,不是直接在原对象上修改,而是返回一个新对象和原对象保持一致并进行此次修改。
