开发中的绝大多数时候,我们并不需要关注 React 项目的性能问题。虽然我们在前面几个章节中,也花了几篇文章来分析如何优化 React 的性能体验,但是这些知识点在开发过程中能用到的机会其实比较少。面试的时候用得比较多。
但是,当你的项目遇到性能瓶颈,如何优化性能就变得非常重要。当然,我们前面几篇文章已经把性能优化的方式和方法说得非常清晰了,大家可以回顾一下。这篇文章我们要分享的重点是,当我采用不同的方式优化之后,代码逻辑执行所要付出的代价到底如何。
例如,当我们得知 React 的 DIFF 是全量比较的时候,可能第一个反应就是觉得他性能差。但是具体性能差到什么程度呢?有没有一个具体的数据来支撑?不确定,这只是一种主观感受。优化之后的性能到底强不强呢,也不敢肯定。
因此,这篇文章主要给大家介绍几种 react 在 diff 过程中用到的比较方式,以及当这几种方式大量执行时,执行所要花费的时间。
一、对象直接比较
又称为全等比较,这是一种成本最低的比较方式。在 React 中,state 与 props 的比较都会用到这样的方式。
var prevProps = {}
var nextProps = {}
if (prevProps === nextProps) {
...
}
那么,这种比较方式的成本有多低呢?我们来写一个循环简单验证一下。分别看看比较一万次需要多长时间。
var markTime = performance.now()
var prev = {}
var next = {}
for(var i = 0; i <= 10000; i++) {
if (prev === next) {
console.log('相等')
}
}
var endTime = performance.now()
console.log(endTime - markTime)
执行结果会有小范围波动,展示出来的结果都是取的多次执行的平均值,或者出现次数最多的执行结果。比如本案例执行,用时最短的是 0.3 ms,用时最长的是 0.8 ms。
可以看到,对比一万次,用时约 0.6ms。
对比一百万次,用时约 6.4ms。
通常情况下,我们项目的规模应该很难超过一万次,控制得好一点,一般都在 1000 次以内。多一点也应该在 5000 次以内,5000 次用这种方式的对比只需要 0.3ms 左右。
二、Object.is
Object.is 是一种与全等比较相似但不同的比较方式,他们的区别就在于处理带符号的 0 和 NaN 时结果不一样。
+0 === -0 // true
Object.is(+0, -0) // false
NaN === NaN // false
Object.is(NaN, NaN) // true
React 源码里为 Object.is 做了兼容处理,因此多了一点判断,所以他的性能上会比全等要差一些。
function is(x, y) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs =
typeof Object.is === 'function' ? Object.is : is;
那么差多少呢?我们先写一个逻辑来看一下执行一万次比较需要多久。
function is(x, y) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs =
typeof Object.is === 'function' ? Object.is : is;
var markTime = performance.now()
var prev = {}
var next = {}
for(var i = 0; i <= 10000; i++) {
if (objectIs(prev, next)) {
console.log('相等')
}
}
var endTime = performance.now()
console.log(endTime - markTime)
执行结果如下,大概是 0.8ms。
执行一百万次,用时约 11.4ms。
那么我们的项目规模在 5000 次比较以内的话,用时估计在 0.4ms 左右,比全等比较多用了 0.1ms。
三、shallowEqual
这种浅比较的成本就稍微大一些,例如,当我们对子组件使用了 memo
包裹之后,那么在 diff 过程中,对于 props 的比较方式就会转变成这样方式,他们会遍历判断 props 第一层每一项子属性是否相等。
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
首先,这种比较方式在 React 中出现的次数非常的少,只有我们手动新增了 memo 之后才会进行这种比较,因此,我们测试的时候,先以 1000 次为例看看结果。
我们定义两个数量稍微多一点的 props 对象,他们最有最后一项不相同,因此比较的次数会拉满。
var prev = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1}
var next = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 2}
完整代码如下:
function is(x, y) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs =
typeof Object.is === 'function' ? Object.is : is;
function shallowEqual(objA, objB) {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!Object.hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
var markTime = performance.now()
var prev = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1}
var next = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 2}
for(var i = 0; i <= 1000; i++) {
if (shallowEqual(prev, next)) {
console.log('相等')
}
}
var endTime = performance.now()
console.log(endTime - markTime)
1000 次比较结果耗时如下,约为 1.4ms。
5000 次比较结果耗时如下,约为 3.6ms。
10000 次比较结果耗时如下,约为 6.6 ms。
这里我们可以做一个简单的调整,让对比耗时直接少一半。那就是把唯一的变化量,写到前面来,如图所示,耗时只用了 3.1ms。
运用到实践中,就是把 props 中的变量属性,尽量写在前面,能够大幅度提高对比性能。
四、总结
次数 | 全等 | is | shallow |
五千 | 0.3 | 0.4 | 3.6 |
一万 | 0.6 | 0.8 | 6.6 |
百万 | 6.4 | 11.4 | 162 |
因此我们从测试结果中看到,全量 diff 并不可怕,如果你对性能优化的理解非常到位,那么能你的项目中,全量 diff 所花费的时间只有 0.几ms,理论的极限性能就是只在你更新的组件里对比出差异,执行 re-render。
当然,由于对于 React 内部机制的理解程度不同,会导致一些差异,例如有些同学的项目中,会执行过多的冗余 re-render。从而导致在大型项目中性能体验可能出现问题。那么这种情况下,也不用担心,有一种超级笨办法,那就是在项目中,结合我们刚才在 shallowEqual 中提高的优化方案,无脑使用 useCallback 与 memo,你的项目性能就能得到明显的提高,当然,这个方案不够优雅但是管用。
可以看出,React 性能优化最重要的手段非常简单:就是控制当前渲染内容的节点规模。