React内部让人迷惑的性能优化策略

【React内部让人迷惑的性能优化策略】大家好,我卡颂。
相比Vue可以基于模版进行编译时性能优化,React作为一个完全运行时的库,只能在运行时谋求性能优化。
这些优化对开发者大多是无感知的,但对项目进行性能优化时也常令开发者困惑。比如如下代码:

function App { const [num, updateNum] = useState(0); console.log('App render', num); useEffect(() => { setInterval(() => { updateNum(1); }, 1000) }, [])return ; }function Child() { console.log('child render'); return child; }

挂载App组件后,会打印几条信息呢?
本文就这个Demo讲解React内部的性能优化策略。
在线Demo地址
欢迎加入人类高质量前端框架群,带飞
性能优化的效果 如果不考虑优化策略,代码运行逻辑如下:
  1. App组件首次render,打印App render 0
  2. 子组件Child首次render,打印child render
  3. 1000ms后,setInterval回调触发,执行updateNum(1)
  4. App组件再次render,打印App render 1
  5. 子组件Child再次render,打印child render
  6. 每过1000ms,重复步骤3~5
实际我们会发现,重复执行步骤3~5不会产生任何变化,这里显然是有优化空间的。
针对这种情况,React确实做了优化。上述Demo会依次打印:
  1. App render 0
  2. child render
  3. App render 1
  4. child render
  5. App render 1
这里让人困惑的点在于:为什么num从0变为1后,App render 1执行了2次,而child render只执行了一次?
接下来,我们从理论和实际角度解释以上原因。
性能优化的理论 在useState文档中提到了一个名词:bailout。
他指:当useState更新的state当前state一样时(使用Object.is比较),React不会render该组件的子孙组件。
注意:当命中bailout后,当前组件可能还是会render,只是他的子孙组件不会render
这是因为,大部分情况下,只有当前组件renderuseState才会执行,才能计算出state,进而与当前state比较。
就我们的Demo来说,只有App renderuseState执行后才能计算出num
function App { // useState执行后才能计算出num const [num, updateNum] = useState(0); // ...省略 }

在useState not bailing out when state does not change #14994中,Dan也反复强调这一观点。
那么从理论看,在我们的Demo中,num从0变为1后,child render只执行了一次是可以理解的,因为App命中了bailout,则他的子组件Child不会render
但是bailout只针对目标组件的子孙组件,那为什么对于目标组件App来说,App render 1执行了2次后就不再执行了呢?
实际的性能优化策略,还要更复杂些。
实际的性能优化策略 React的工作流程可以简单概括为:
  1. 交互(比如点击事件useEffect)触发更新
  2. 组件树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 ; }

正常情况,updateNum(1)执行,触发更新。直到App renderuseState执行后才会计算出新的num,进而与当前的num比较,判断是否命中bailout
如果updateNum(1)执行后,立刻计算出新的num,进而与当前的num比较,如果相等则组件树都不会render
这种将计算state的时机提前的策略,叫eagerState(急切的state)。
总结 综上所述,我们的Demo是混合了这两种优化策略后的结果:
  1. App render 0(未命中策略)
  2. child render
  3. App render 1(未命中策略)
  4. child render
  5. App render 1(命中bailout
  6. (命中eagerState
  7. (命中eagerState
......
bailout的实现细节参考React组件到底什么时候render啊。
限于篇幅有限,eagerState的实现细节会单开一篇文章讨论。

    推荐阅读