7

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

 2 years ago
source link: https://segmentfault.com/a/1190000041483901
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

大家好,我卡颂。

相比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
  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 <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的实现细节会单开一篇文章讨论。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK