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)