React文档(十八)最佳性能

时间:2023-12-12 22:22:44

在内部,React使用好几种聪明的技巧去最小化更新UI所需要的DOM操作。对于很多应用来说,使用React会使得构建用户界面非常之快而且不需要做太多专门的性能优化。虽然如此,还是有一些方法可以让你为React应用加速。

使用生产构建

如果你正在性能测试或者在你的应用里遇到性能测试问题,确保你测试时使用了压缩了的生产构建:

  • 对于创建React应用,你需要运行npm run build然后遵循指令
  • 对于单文件构建,我们提供生产环境.min.js的文件版本
  • 对于模块管理,你需要设置NPDE_ENV=production
  • 对于webpack,你需要添加这个到配置文件的plugins里
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin()
  • 对于汇总,你需要在commonjs插件之前使用replace插件因此只在开发环境使用的模块就不会被导入。完整的设置例子请看这里
plugins: [
require('rollup-plugin-replace')({
'process.env.NODE_ENV': JSON.stringify('production')
}),
require('rollup-plugin-commonjs')(),
// ...
]

开发的构建包括了额外的警告很有帮助但是由于额外的统计所以会让程序变慢。

使用chrome performance对组件进行性能分析

在开发模式下,你可以通过使用浏览器里的性能工具来显示组件的实例化,更新和销毁。举个例子:

React文档(十八)最佳性能

在chrome浏览器里这样做:

  1. 加载你的应用,使地址url的查询字符串为?react_perf
  2. 打开chrome开发者工具的performance面板并且按下record
  3. 然后做一些你想要测试分析的动作。不要录制超过20秒否则chrome可能会挂起
  4. 停止录制
  5. React事件将会成组地出现在user timing标签下面

注意那些数字是相对的因此组件在生产环境下会渲染地更快。还有,这样可以帮助你意识到不相关的UI会错误的更新,还有UI更新的深度和频率。

如今的chrome,edge和IE浏览器支持这个特性,但是我们使用的标准user timing API因此我们希望更多的浏览器可以添加对它的支持。

避免重复渲染

React在渲染出的UI内部建立和维护了一个内层的实现方式。这个内部表示包含了从组建里返回的React元素。这个内部表示让React避免了不必要的创建和关联DOM节点,那样会使速度变慢。有时被提到为“虚拟DOM”,但是在React Native里它同样存在。

当一个组件的props或者state改变了,React通过比较新返回的元素和之前渲染的元素来决定是否一个DOM的更新是必要的。当两者不一样的时候,React会更新DOM。

在一些情况下,你的组件通过重写生命周期函数shouldComponentUpdate可以为程序加速,shouldComponentUpdate是在重新渲染的流程开始之前被触发。这个函数默认会返回true,委托React去更新:

shouldComponentUpdate(nextProps, nextState) {
return true;
}

如果你知道在某些情况下你的组件不需要更新,你可以在shouldComponentUpdate里返回false,来跳过整个渲染流程,包括对该组件和之后的内容调用render()方法。

shouldComponentUpdate应用

下面是组件的树状目录。对于每一个节点,SCU指明了shouldComponentUpdate返回了什么,而vDOMEq指明了是否已经渲染的React元素发生了变化。最终,圆圈的颜色表明了是否组件需要重新渲染。

React文档(十八)最佳性能

自从shouldComponentUpdate在树的节点C2处返回了false,React不会试图渲染C2节点,因此在C4和C5节点上也不需要调用shouldComponentUpdate。

对于C1和C3节点,shouldComponentUpdate返回了true,因此React必须往下到叶子节点去检查它们。对于C6shouldComponentUpdate返回了true,自从元素已经发生改变React就必须更新DOM。

最有趣的情况是C8。React必须渲染这个组件,但是自从React元素返回的和之前渲染的一样,那就不必更新DOM。

注意React只是必须改变C6的DOM,这是不可避免的。对于C8,它通过比较跳出了更新,并且对于C2的子树和C7,甚至不需要比较因为shouldComponentUpdate返回了false,所以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和stete.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会做一个简单的比较在新的和旧的this.props.words之间。自从WordAdder类里的handleClick方法里的words数组发生了改变,旧的和新的this.props.words的值会比较为相同的,即使数组中的单词真的发生了变化。ListOfWords因此就不会更新即使它拥有了新的单词。

不会突变的数据的力量

最简单的方法去避免这个问题就是避免去使用可能会突变的props或者state。举个例子,上面的handleClick方法可以使用concat重写:

handleClick() {
this.setState(prevState => ({
words: prevState.words.concat(['marklar'])
}));
}
ES6支持一种对于数组的扩展操作符可以让这里更简单。如果你是创建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。

这里有一个js建议要添加对象扩展操作符使得不修改而更新对象更加简便:

function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}

如果你正在创建React App,Object.assign和扩展操作符语法都默认是可用的。

使用不可变的数据结构

Immutable.js是解决这个问题的另一种方法。它通过结构共享提供不可突变的,持久的集合:
  • 不可变的:一旦创建,一个合集在其他时间点不能被改变。
  • 执着的:新的合集可以通过前一个合集和一个改变来建立。原始的集合在新的集合建立后依然可用。
  • 结构分享:新的合集尽可能多的使用和原始集合同样的结构来创建,减少复制到最低限度来提高性能。

不可变性使得追踪改变很简单。每个变化都会导致产生一个新的对象,因此我们只需检查索引对象是否改变。举个例子,在这段js代码中:

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');
x === y; // false

在这个例子中,自从改变了x一个新的引用返回,我们可以设想x被改变了。

另外两个可以帮助我们使用不可改变的数据的库是seamless-immutable和immutability-helper。

不可变的数据结构提供了方便的方式来追踪对象的变化,这就是我们需要的东西来实现shouldComponentUpdate。这样你就可以获得一个很好的性能提高。