您的位置:首页 > Web前端 > React

React学习笔记(五):性能优化

2018-05-24 00:58 363 查看

    React内部,使用了许多有趣的技术去减少更新UI的DOM操作消耗。对于很多应用,React可以提供更快的用户界面而不需要特意的去优化。尽管如此,你还是有很多方法去优化你的APP。

(一)使用生产构建

    如果你经历过React APP的表现问题,请确认你测试的是压缩过的生产环境版本。默认情况下,React会提供很多有用的预警信息——在开发阶段非常有用。但是它们使React应用变得更大和更慢,所以你需要保证在部署应用的时候使用的是生产版本。如果你不太确定构建过程是否正确,可以通过下载React开发者工具在Chrome浏览器中进行检测。

1)创建React应用

npm run build

    EZ,这个命令会在build/目录中为你的应用创建一个生产版本,在部署应用之前使用就行,平常开发请使用 npm start

    md感觉这里的内容好麻烦——想想“大家都会有什么学头”,继续吧

2)单文件版本(Single-File Builds)

    我们提供了为React和React DOM提供了随时可以投入生产的版本(作为独立文件)。请注意:只有以.production.min.js结尾的文件可以在生产环境中使用:

<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>

3)针对个别工具的优化

Brunch

    如果你使用更有效率的Brunch生产版本(Brunch是一个类似Gulp、Webpack的构建工具,封装的层级更高,使用起来更方便),可以使用uglify-js-brunch插件:
# If you use npm
npm install --save-dev uglify-js-brunch

# If you use Yarn
yarn add --dev uglify-js-brunch
    构建应用的时候,使用如下命令:
brunch build -p
    注意:不要在开发环境中使用-p选项,这样只会掩盖有用的警告信息并且使构建过程变得更慢。

Browserify

    原理同上,下载插件 + 使用插件

# If you use npm
npm install --save-dev envify uglify-js uglifyify

# If you use Yarn
yarn add --dev envify uglify-js uglifyify

  • envify:保证设置了正确的构建环境;
  • uglifyify:将开发工具移除;
  • uglify-js:最后的文件块将会被导入uglify-js进行最后的处理(mangling,解决冲突问题)

    构建命令:

browserify ./index.js \
-g [ envify --NODE_ENV production ] \
-g uglifyify \
| uglifyjs --compress --mangle > ./bundle.js
    “注意”同上。

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')(),
// ...
]
    “注意”同上。

Webpack

    注意:如果你使用create-react-app,不需要使用这里的方法。这里只和直接通过webpack配置的项目相关。

    将下列插件加入配置文件:

new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new webpack.optimize.UglifyJsPlugin()

(二)通过Chrome的性能选项卡分析组件

    在开发者模式中,你可以观察到组件的挂载、更新、卸载:


    使用方式:

  1. 临时禁用所有Chrome插件,尤其是React开发者工具——他们会显著地歪曲结果;
  2. 确保你在开发者模式中运行应用;
  3. 打开Chrome开发者工具:Performance——Record(开始记录);
  4. 执行你想要记录的动作,尽量将操作限制在20s以内,否则Chrome可能挂起;
  5. 停止记录;
  6. React事件将会聚集在User Timing标签下面;

    想要更深入的了解,可以看看这篇博客

    注意:所有数字都是相对的,组件在生产环境中会渲染的更快。但是,这样会帮助了解因为各种错误导致的不相关UI的渲染、以及这种UI更新的深度与频率。

    只有当前的Chrome、IE、Edge支持这些特性,但是,我们使用标准User Timing API——可以预见会有原来越多的浏览器会在未来支持它。

(三)虚拟化长列表

    如果你的应用渲染一个很长的数据列表(可能成百上千行),我们建议使用“窗口化(windowing)”技术——他在一个给定时间只会渲染数据列表的一个子集,可以显著的节约当创建DOM节点的时候重新渲染组件的时间。

    React virtualized是一个很受欢迎的窗口化库,它提供了许多可复用的组件去展示列表、网格、和扁平化的数据。如果你想要更适合于你的应用的一些特性,你也可以创建自己的窗口化组件,比如Twitter

(四)拒绝妥协

    React构建并保持了所渲染UI的一个内部表示形式,包括从你的组建中返回的React元素。这个内部表示使得React可以避免非必要的创建DOM节点、或者取得DOM节点——这样(?)会比操作JavaScript对象更慢。一些情况下它被称为“虚拟DOM”,但实质上它和React Native的工作方式一样。

    当一个组件的props或者state改变的时候,React通过比较新返回元素和之前渲染的,去决定是否需要一个真是的DOM更新。当他们不想等的时候,React就会更新DOM。

    你可以通过React开发者工具观察到虚拟DOM的重新渲染。

    (下载使用方式自己百度吧)


    注意上面使用的例子:(当你和页面交互的时候,被重新渲染的组件会出现彩色边框)当我们输入第二行数据的时候,第一行数据也出现了彩色边框——它也被重新渲染了,这时我们可以称它为“被浪费了的”渲染——我们知道这种渲染是没有必要的,因为第一行数据根本没变——但是React不知道。那怎么才能然它知道呢?

    尽管React只会更新改变了的DOM节点,重新渲染仍然会耗费一定时间,在大多数情况下这不是问题,但是,如果这种缓慢是可感知的,你也可以通过重写生命周期函数shouldComponentUpdate()(会在重新渲染过程开始前被触发)去手动提速。这个方法默认的实现会返回true——让React执行更新操作。

shouldComponentUpdate(nextProps, nextState) {
return true;
}
    如果你知道组件在某些情况下不需要更新,你可以返回false,这样可以跳过整个渲染过程——包括调用组件的render()方法及以下的代码。

    在大多数情况下,除了手写shouldComponentUpdate()方法,你还可以继承React.PureComponent,它等同于实现一个深度比较过去和当前props、state值的shouldComponentUpdate()方法。

(四)shouldComponentUpdate 方法的工作方式

    

    这是一个组件的子树,对于每一个节点,SCU代表shouldComponentUpdate返回的值,vDOMEq代表渲染的React元素是否(与之前)相同。最后,节点的颜色代表组件是否需要调整。

    因为对于以C2为根节点的子树,C2对于shouldComponentUpdate返回false,React不会渲染C2,因此也不会对C4\C5调用shouldComponentUpdate方法。对于C1和C3,shouldComponentUpdate返回true,所以React还需要向下遍历叶节点去继续检查。对于C6,方法还是返回true,并且渲染的元素与之前并不相同——React会更新DOM。

    对于C8,React会渲染这个组件(因为shouldComponentUpdate返回true),但是因为React元素返回的值和之前渲染的一样,所以并不会更新DOM。

    注意:React只需要对C6进行DOM操作,并且必须这样。对于C8,通过对于渲染的React组件决定不操作;对于C7,都不必比较渲染的React组件——由于shouldComponentUpdate返回false,render函数都不会被执行。

(五)例子

    如果你的组件只会在props.color、state.count变量改变的时候变化,你可以这样定义它:

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>
);
}
}
    对于更复杂的应用,你可以使用同样的模式去做对所有的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只会简单的比较this.props.words中的新旧值。因为这段代码改变了在WordAdder的handleClick方法中的words数组,this.props.words的新旧值会被比为相等的,即使数组中真正的改变了。这个ListOfWords组件将不会被更新——尽管它有了得到了新的词语。(const words = this.state.words声明的是一个引用,会随着this.state.words的值的变化而变化,反之也是。向任何一个引用中添加词语,作用的都是相同的对象——而对象和自身一定相等)

(六)不改变数据(Not Mutating Data)的力量

    避免这种问题的最简单方式就是在使用props或者state的时候尽量避免改变值。比如可以将上面的方法改写为如下形式:

handleClick() {
this.setState(prevState => ({
words: prevState.words.concat(['marklar'])
}));
}
    ES6支持对数组使用更简洁的语法:
handleClick() {
this.setState(prevState => ({
words: [...prevState.words, 'marklar'],
}));
};
    对于对象,你也可以使用相似的方法。比如,我们有一个名为colormap的对象,并且想要写一个方法将colormap.right设置为'blue',我们可能会写:
function updateColorMap(colormap) {
colormap.right = 'blue';
}
    想要实现不改变原始对象,我们可以使用Object.assign方法(ES6方法,需要polyfill):
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
    有一种JavaScript的建议是添加“对象传播属性”(object spread properties)去使得更新对象更容易,并且避免了改变原始对象。

    注意:如果你使用create-react-app,Object.assign、和对象传播语法(object spread syntax)都默认可用。

(七)使用不变的数据结构

    Immutalbe.js是对于这种问题的另一种解决方式,它提供了通过结构的共享实现的,不变的、持久的集合:

  • 不变的:一旦创建,集合不可以在任何时间点被改变;
  • 持久的:新集合可以被一个老集合+可变项(比如 Set)创建,这个原始的集合在新集合创建之后同样是合法的;(?)
  • 结构的共享:新集合被创建时会尽可能的适用和原来集合相同的结构,减少拷贝以提高效率。

    不变性使得跟踪变化开销很小,一个变化总是会创建一个新的对象,所以我们只要检查对于这个对象的引用变化了就行,比如,对于一段一般的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的时候,一个新的引用就被创建。我们可以使用一个比较引用是否相等的检查去确定储存在y中的新值与之前储存在x中的是不同的。

    其他两个可以帮助我们使用不可变数据的是:seamless-immutableimmutability-helper

    不可变数据结构提供了一个简单的方法去跟踪对象的改变——我们需要它去实现shouldComponentUpdate方法。这些方法总是可以提供一个不错的性能进步。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: