React通过虚拟DOM、高效的Diff算法等技术极大地提高了操作DOM的效率。在大多数场景下,我们是不需要考虑React程序的性能问题的,但只要是程序,总会有一些优化的措施。本章就来介绍一下React中常用的性能优化方式。
使用生产版本
这是性能优化的一个基本原则,也是很容易被忽视的一个原则。我们使用CRA脚手架创建的项目,在以npm run start启动时,使用的React是开发环境版本的React库,包含大量警告消息,以帮助我们在开发过程中避免一些常见的错误,比如组件props类型的校验等。开发环境版本的库不仅体积更大,而且执行速度也更慢,显然不适合在生产环境中使用。那么,如何构建生产环境版本的React库呢?对于CRA脚手架创建的项目,只需要执行npm run build,就会构建生产环境版本的React库。其原理是,一般第三方库都会根据process.env.NODE_ENV这个环境变量决定在开发环境和生产环境下执行的代码有哪些不同,当执行npm run build时,构建脚本会把NODE_ENV的值设置为production,也就是会以生产环境模式编译代码。
如果不是使用create-react-app脚手架创建的项目,而是完全自己编写Webpack的构建配置,那么在执行生产环境的构建时,就需要在Webpack的配置项中包含以下插件的配置:
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
new UglifyJSPlugin()
//...
];
当NODE_ENV等于production时,不仅是React,项目中使用到的其他库也会执行生产环境版本的构建。但一定要注意,在开发过程中不要执行这项设置,因为它会让你失去很多重要的调试信息,并且代码的编译速度也会变慢。
虚拟化长列表
如果你的应用渲染了长列表(上百甚至上千的数据),我们推荐使用“虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。
react-window 和 react-virtualized 是热门的虚拟滚动库。 它们提供了多种可复用的组件,用于展示列表、网格和表格数据。如果你想要一些针对你的应用做定制优化,你也可以创建你自己的虚拟滚动组件。
避免不必要的组件渲染
当组件的props或state发生变化时,组件的render方法会被重新调用,返回一个新的虚拟DOM对象。但在一些情况下,组件是没有必要重新调用render方法的。例如,父组件的每一次render调用都会触发子组件componentWillReceiveProps的调用,进而子组件的render方法也会被调用,但是这时候子组件的props可能并没有发生改变,改变的只是父组件的props或state,所以这一次子组件的render是没有必要的,不仅多了一次render方法执行的时间,还多了一次虚拟DOM比较的时间。
React组件的生命周期方法中提供了一个shouldComponentUpdate方法,这个方法的默认返回值是true,如果返回false,组件此次的更新将会停止,也就是后续的componentWillUpdate、render等方法都不会再被执行。我们可以把这个方法作为钩子,在这个方法中根据组件自身的业务逻辑决定返回true还是false,从而避免组件不必要的渲染。例如,我们通过比较props中的一个自定义属性item,决定是否需要继续组件的更新过程,代码如下:
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.item === this.props.item) {
return false;
}
return true;
}
// ...
}
注意:示例中对item的比较是通过===比较对象的引用,所以即使两个对象的引用不相等,它们的内容也可能是相等的。最精确的比较方式是遍历对象的每一层级的属性分别比较,也就是进行深比较,但shouldComponentUpdate被频繁调用,如果props和state的对象层级很深,深比较对性能的影响就比较大。一种折中的方案是,只比较对象的第一层级的属性,也就是执行浅比较。例如下面两个对象:
const item = { foo, bar };
const nextItem = { foo, bar };
执行浅比较会使用===比较item.foo和nextItem.foo、item.bar和nextItem.bar,而不会继续比较foo、bar的内容。React中提供了一个PureComponent组件,这个组件会使用浅比较来比较新旧props和state,因此可以通过让组件继承PureComponent来替代手写shouldComponentUpdate的逻辑。但是,使用浅比较很容易因为直接修改数据而产生错误,例如:
class NumberList extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
numbers: [1, 2, 3, 4]
};
this.handleClick = this.handleClick.bind(this);
}
// numbers中新加一个数值
handleClick() {
const numbers = this.state.numbers;
// 直接修改numbers对象
numbers.push(numbers[numbers.length - 1] + 1);
this.setState({ numbers: numbers });
}
render() {
return (
<div>
<button onClick={this.handleClick} />
{this.state.numbers.map((item) => (
<div>{item}</div>
))}
</div>
);
}
}
点击Button,NumberList并不会重新调用render,因为handleClick中是直接修改this.state.numbers这个数组的,this.state.numbers的引用在setState前后并没有发生改变,所以shouldComponentUpdate会返回false,从而终止组件的更新过程。在第4章深入理解组件state中,我们讲到要把state当作不可变对象,一个重要的原因就是为了提高组件state比较的效率。对于不可变对象来说,只需要比较对象的引用就能判断state是否发生改变。
列表使用key
列表元素定义了key,React会根据key索引元素,在render前后,拥有相同key值的元素是同一个元素,例如前面举过的例子:
// render前
<ul>
<li key="first" >first</li>
<li key="second">second</li>
</ul>
// render后
<ul>
<li key="third">third</li>
<li key="first">first</li>
<li key="second">second</li>
</ul>
定义key之后,React并不会“傻瓜式”地按顺序依次更新每一个li元素:把第一个li元素更新为third,把第二个li元素更新为first,最后创建一个新的li元素,内容为second。有了key的索引,React知道first和second这两个li元素并没有发生变化,而只会在这两个li元素前面插入一个内容为third的li元素。可见,key的使用减少了DOM操作,提高了DOM更新效率。当列表元素数量很多时,key的使用更显得重要。
总结
性能优化方法是最常用的这几种方法,其中使用生产版本是项目中必须采用的,使用虚拟化长列表和key也推荐在项目中采用。通过重写shouldComponentUpdate方法避免不必要的组件渲染,这在项目开始阶段是可以不必在意的,大多数情况下,组件只是重复调用render方法对于性能的影响并不大。当发现项目确实存在性能问题时,再考虑通过这种方式进行优化也不迟。请大家记住,过早的优化并不是
一件好事。
评论 (0)