React内部让人迷惑的性能优化策略
The following article is from 魔术师卡颂 Author 卡颂
点击关注公众号,一周多次包邮送书
相比Vue
可以基于模版进行「编译时性能优化」,React
作为一个完全运行时的库,只能在「运行时」谋求性能优化。
这些优化对开发者大多是「无感知」的,但对项目进行「性能优化」时也常令开发者困惑。比如如下代码:
function App {
const [num, updateNum] = useState(0);
console.log('App render', num);
useEffect(() => {
setInterval(() => {
updateNum(1);
}, 1000)
}, [])
return <Child/>;
}
function Child() {
console.log('child render');
return <span>child</span>;
}
挂载App
组件后,会打印几条信息呢?
本文就这个Demo
讲解React
内部的「性能优化策略」。
在线Demo地址[1]
性能优化的效果
如果不考虑「优化策略」,代码运行逻辑如下:
App
组件首次render
,打印「App render 0」子组件
Child
首次render
,打印「child render」1000ms后,
setInterval
回调触发,执行updateNum(1)
App
组件再次render
,打印「App render 1」子组件
Child
再次render
,打印「child render」每过1000ms,重复步骤3~5
实际我们会发现,重复执行步骤3~5不会产生任何变化,这里显然是有优化空间的。
针对这种情况,React
确实做了优化。上述Demo
会依次打印:
App render 0
child render
App render 1
child render
App render 1
这里让人困惑的点在于:为什么num
从0变为1后,App render 1
执行了2次,而child render
只执行了一次?
接下来,我们从「理论」和「实际」角度解释以上原因。
性能优化的理论
在useState文档[2]中提到了一个名词:「bailout」。
他指:当useState
更新的state
与当前state
一样时(使用Object.is
比较),React
不会render
该组件的「子孙组件」。
注意:当命中bailout
后,当前组件可能还是会render
,只是他的「子孙组件」不会render
。
这是因为,大部分情况下,只有当前组件render
,useState
才会执行,才能计算出state
,进而与当前state
比较。
就我们的Demo
来说,只有App render
,useState
执行后才能计算出num
:
function App {
// useState执行后才能计算出num
const [num, updateNum] = useState(0);
// ...省略
}
在useState not bailing out when state does not change #14994[3]中,Dan
也反复强调这一观点。
那么从理论看,在我们的Demo
中,num
从0变为1后,「child render只执行了一次」是可以理解的,因为App
命中了bailout
,则他的子组件Child
不会render
。
但是bailout
只针对「目标组件的子孙组件」,那为什么对于目标组件App
来说,App render 1
执行了2次后就不再执行了呢?
实际的性能优化策略,还要更复杂些。
实际的性能优化策略
React
的工作流程可以简单概括为:
交互(比如
点击事件
、useEffect
)触发更新组件树
render
刚才讲的bailout
发生在步骤2:组件树开始render
后,命中了bailout
的组件的子孙组件不会render
。
实际还有一种更「前置」的优化策略:当步骤1触发更新时,发现state
未变化,则根本不会继续步骤2。
从我们的Demo
来说:
function App {
const [num, updateNum] = useState(0);
console.log('App render', num);
useEffect(() => {
setInterval(() => {
updateNum(1);
}, 1000)
}, [])
return <Child/>;
}
正常情况,updateNum(1)
执行,触发更新。直到App render
,useState
执行后才会计算出新的num
,进而与当前的num
比较,判断是否命中bailout
。
如果updateNum(1)
执行后,立刻计算出新的num
,进而与当前的num
比较,如果相等则组件树都不会render
。
这种「将计算state的时机提前」的策略,叫eagerState
(急切的state
)。
总结
综上所述,我们的Demo
是混合了这两种优化策略后的结果:
App render 0(未命中策略)
child render
App render 1(未命中策略)
child render
App render 1(命中
bailout
)(命中
eagerState
)(命中
eagerState
)
......
bailout
的实现细节参考React组件到底什么时候render啊。
限于篇幅有限,eagerState
的实现细节会单开一篇文章讨论。
参考资料
[1]在线Demo地址:https://codesandbox.io/s/optimistic-wescoff-gx3ooc?file=/src/App.js
[2]useState文档:https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
[3]useState not bailing out when state does not change #14994:https://github.com/facebook/react/issues/14994#issuecomment-472840510
推荐阅读
• 推荐 5 个 YYDS 的 Java 项目• 数据库精选 60 道面试题• 阿里面试:周芷若和韩小昭的并查集• 一个悄然成为世界最流行的操作系统• 通过几行 JS 就可以读取电脑上的所有数据?• 12 张图带你彻底理解 ZGC• 难缠的Redis主从复制原理,这次通透了!
👇更多内容请点击👇