事实证明,RFC 流程非常有帮助,它作为一个思想框架,迫使我们充分考虑到了潜在变革的所有方面,并允许我们的社区参与到设计过程中,提交深思熟虑的功能请求。
更快、更小性能对于前端框架来说是至关重要的。尽管 Vue 2 拥有极具竞争力的性能,但通过实验新的渲染策略,重写提供了一个更进一步的机会。
克服虚拟 DOM 的瓶颈Vue 有一个相当独特的渲染策略。它提供了一个类似于 HTML 的模板语法,但将模板编译成了返回虚拟 DOM 树的渲染函数。该框架通过递归地走过两个虚拟 DOM 树,并比较每个节点上的每一个属性,计算出实际 DOM 的哪些部分需要更新。由于现代 JavaScript 引擎进行了高级优化,这种有点蛮力的算法一般来说是相当快的,但是更新仍然会涉及到很多不必要的 CPU 工作。当你看一个基本上是静态内容和只有几个动态绑定的模板时,效率低下的问题就特别明显--整个虚拟 DOM 树仍然需要递归地走一遍,以找出改变了什么。
幸运的是,模板编译步骤让我们有机会对模板进行静态分析并提取动态部分的信息。Vue 2 通过跳过静态子树在一定程度上做到了这一点,但由于编译器架构过于简单化,更高级的优化很难实现。在 Vue 3 中,我们用适当的 AST transform pipeline 重写了编译器,这使得我们可以用变换插件的形式来进行编译时的优化。
有了新的架构,我们希望找到一种能够尽可能消除开销的渲染策略。一个选择是抛弃虚拟 DOM,直接生成必要的 DOM 操作,但这将消除直接编写虚拟 DOM 渲染函数的能力,我们发现这对高级用户和库作者来说是非常有价值的。另外,这将是一个巨大的突破性改变。
接下来最好的办法就是去掉不必要的虚拟 DOM 树遍历和属性比较,这些往往在更新过程中的性能开销最大。为了实现这个目标,编译器和运行时需要协同工作。编译器分析模板,并生成带有优化提示的代码,而运行时则接收这些提示并尽可能地采取快速路径。这里有三个主要的优化工作。
首先,在树级,我们注意到,在没有模板指令动态改变节点结构的情况下,节点结构保持完全静态(例如,v-if 和 v-for)。如果我们把一个模板划分成由这些结构指令分隔的嵌套 "块",那么每个块内的节点结构又变得完全静态。当我们更新块内的节点时,我们不再需要递归地遍历块内的树形动态绑定,可以在一个平面数组中跟踪。这种优化避免了虚拟 DOM 的大部分开销,减少了我们需要执行的树状遍历量,减少了一个数量级。
其次,编译器会主动检测模板中的静态节点、子树,甚至是数据对象,并在生成的代码中把它们挂在渲染函数之外。这就避免了在每次渲染时重新创建这些对象,极大地提高了内存使用量,减少了垃圾回收的频率。
第三,在元素层面,编译器还会根据每个具有动态绑定的元素需要执行的更新类型,为其生成一个优化标志。例如,一个具有动态类绑定和多个静态属性的元素将收到一个标志,提示只需要进行类检查。运行时将接收到这些提示并采取专用的快速路径。
CPU 时间 即执行 JavaScript 计算所花费的时间,不包括浏览器 DOM 操作。
综合起来,这些技术大大改善了我们的渲染更新基准,Vue 3 有时只需要不到 Vue 2 的十分之一的 CPU 时间。
最大限度地减少软件包的大小框架的大小也会影响到它的性能。这对于 Web 应用来说是一个独特的问题,因为资产需要实时下载,在浏览器解析了必要的 JavaScript 之后,应用程序才会进行交互。这对于单页面应用来说尤其如此。虽然 Vue 一直以来都是相对轻量级的--Vue 2 的运行时大小约为 23 KB gzipped,但我们注意到了两个问题。
首先,不是每个人都会使用该框架的所有功能。例如,一个从未使用过过渡功能的应用仍然要支付过渡相关代码的下载和解析成本。
第二,随着我们添加新的功能,框架不断地无限增长。当我们考虑增加新功能的权衡时,这就赋予了捆绑大小不成比例的权重。因此,我们倾向于只包含大多数用户会使用的功能。
理想的情况下,用户应该能够在构建的时候,为未使用的框架功能丢弃代码,也就是所谓的 "动摇树"--只为他们使用的功能付费。这也可以让我们在不增加其他用户的付费成本的情况下,为一部分用户提供有用的功能。