前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React内部让人迷惑的性能优化策略

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

作者头像
公众号@魔术师卡颂
发布2022-03-14 16:25:30
7590
发布2022-03-14 16:25:30
举报
文章被收录于专栏:魔术师卡颂

大家好,我卡颂。

相比Vue可以基于模版进行「编译时性能优化」React作为一个完全运行时的库,只能在「运行时」谋求性能优化。

这些优化对开发者大多是「无感知」的,但对项目进行「性能优化」时也常令开发者困惑。比如如下代码:

代码语言:javascript
复制
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]

性能优化的效果

如果不考虑「优化策略」,代码运行逻辑如下:

  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文档[2]中提到了一个名词:「bailout」

他指:当useState更新的state当前state一样时(使用Object.is比较),React不会render该组件的「子孙组件」

注意:当命中bailout后,当前组件可能还是会render,只是他的「子孙组件」不会render

这是因为,大部分情况下,只有当前组件renderuseState才会执行,才能计算出state,进而与当前state比较。

就我们的Demo来说,只有App renderuseState执行后才能计算出num

代码语言:javascript
复制
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的工作流程可以简单概括为:

  1. 交互(比如点击事件useEffect)触发更新
  2. 组件树render

刚才讲的bailout发生在步骤2:组件树开始render后,命中了bailout的组件的子孙组件不会render

实际还有一种更「前置」的优化策略:当步骤1触发更新时,发现state未变化,则根本不会继续步骤2。

从我们的Demo来说:

代码语言:javascript
复制
function App {
  const [num, updateNum] = useState(0);
  console.log('App render', num);

  useEffect(() => {
    setInterval(() => {
      updateNum(1);
    }, 1000)
  }, [])

  return <Child/>;
}

正常情况,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的实现细节会单开一篇文章讨论。

参考资料

[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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 魔术师卡颂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 性能优化的效果
  • 性能优化的理论
  • 实际的性能优化策略
  • 总结
    • 参考资料
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档