为什么Vue3不使用时间切片(Time Slicing)

6,017 阅读4分钟

翻译自Vuejs issue中尤大的回答:原文地址

在Web应用程序中,丢帧(janky)更新通常是由于同步的高CPU时间和原生DOM的更新造成的。

时间切片是在CPU工作期间保持应用响应的一种尝试,但它只影响CPU工作。DOM的刷新必须是同步的,以确保最终DOM状态的一致性。

所以,想象两种丢帧更新的场景:

  1. CPU工作时间在16ms以内,但是需要操作大量的原生DOM更新操作(例如,挂在大量新的DOM内容)。不管你有没有使用时间切片,应用依旧会感觉到掉帧。
  2. CPU任务非常繁重,需要超过16ms的时间。从理论上讲,时间切片开始发挥作用了。然而,HCI的研究表明,除非它在进行动画,否则对于正常的用户交互,大多数人对于100毫秒内的更新是感觉不到有什么不同的。

也就是说,只有在频繁进行超过100ms的纯CPU任务更新时,时间切片才实际有用。

有趣的地方在于,这样的场景更经常地发生在React中,因为:

  1. React的虚拟DOM操作(reconciliation )天生就比较慢,因为它使用了大量的Fiber架构;

  2. React使用JSX来渲染函数相对较于用模板来渲染更加难以优化,模板更易于静态分析。

  3. React Hooks将大部分组件树级优化(即防止不必要的子组件的重新渲染)留给了开发人员,开发人员在大多数情况下需要显式地使用useMemo。而且,不管什么时候React接收到了children属性,它几乎总要重新渲染,因为每次的子组件都是一棵新的vdom树。这意味着,一个使用Hook的React应用在默认配置下会过度渲染。更糟糕的是,像useMomo这类优化不能轻易地自动应用,因为:

    1. 它需要正确的deps数组;
    2. 盲目地任意使用它可能会阻塞本该进行的更新,类似与PureComponent

    不幸的是,大多数开发人员都很懒,不会积极地优化他们的应用。所以大多数使用Hook的React应用会做很多不必要的CPU工作。

相比之下,Vue就上面的问题做一下比较:

  1. 本质上更简单,因此虚拟DOM操作更快(没有时间切片-> 没有fiber-> 更少的开销);

  2. 通过分析模板进行了大量的AOT优化,减少了虚拟DOM操作的基本开销。Benchmark显示,对于一个典型的DOM代码块来说,动态与静态内容的比例大约是1:4,Vue3的原生执行速度甚至比Svelte更快,在CPU上花费的时间不到React的1/10。

  3. 智能组件树级优化通过响应式跟踪,将插槽编译成函数(避免子元素重复渲染)和自动缓存内联句柄(避免内联函数重复渲染)。除非必要,否则子组件永远不需要重新渲染。这一切不需要开发人员进行任何手动优化。

    这意味着对于同一个更新,React应用可能造成多个组件重新渲染,但在Vue中大部分情况下只会导致一个组件重新渲染。

默认情况下, Vue3应用比React应用花费更少的CPU工作时间, 并且CPU工作时间超过100ms的机会大幅度减少了,除非在一些极端的情况下,DOM可能成为更主要的瓶颈。

现在,时间切片或并发模式带来了另一个问题:因为框架现在安排和协调了所有更新,它在优先级、失效、重新实例化等方面产生了大量额外的复杂性。所有这些逻辑处理都不可能被tree-shaken,这将导致运行时基线的大小膨胀。即使包含了Suspense和所有的tree-shaken,Vue 3的运行时仍然只有当前React + React DOM的1/4大小。

注意,这并不是说并发模式是一个不好的主意。它提供一个有趣的新方法解决某一类问题(尤其是相关协调异步状态转换),但时间切片(作为并发的一个子特性)特别解决了React中比其他框架更突出的问题,同时也带来了成本。对于Vue 3来说,这种权衡似乎是不值得的。