一、Vue 3 编译优化的核心背景
Vue 2 的虚拟 DOM 存在一个核心问题:每次数据更新时,会对整个虚拟 DOM 树 进行全量对比(diff),哪怕大部分节点是静态的(不会变),也会被遍历对比,造成性能浪费。
Vue 3 的编译优化核心思路是:在编译阶段提前分析模板,给节点打标记、做预处理,让运行时的 diff 只对比「真正需要更新的节点」,从「全量 diff」变成「定向 diff」,大幅提升性能。
二、核心编译优化点详解
- 静态提升(Static Hoisting)
概念
把模板中不会随数据变化的静态节点 / 属性 (比如纯文本、固定样式、无绑定的标签)从渲染函数中「提升」出去,只在初始化时创建一次,后续更新时复用,避免每次渲染都重新创建虚拟 DOM 节点。
对比示例
- Vue 2 编译结果(伪代码)
js
// 每次渲染都会重新创建 <div>Hello</div> 这个静态节点
function render() {
return createVNode('div', null, [
createVNode('div', null, 'Hello'), // 静态节点,每次都重建
createVNode('span', null, state.msg) // 动态节点
])
}
- Vue 3 编译结果(伪代码):
js
// 静态节点被提升到渲染函数外部,只创建一次
const hoisted = createVNode('div', null, 'Hello')
function render() {
return createVNode('div', null, [
hoisted, // 复用已创建的静态节点
createVNode('span', null, state.msg)
])
}
实际模板示例
vue
<template>
<!-- 静态节点:无任何动态绑定,会被提升 -->
<div class="title">Vue 3 编译优化</div>
<!-- 动态节点:依赖 msg 数据 -->
<div>{{ msg }}</div>
</template>
Vue 3 编译时会把 <div class="title">Vue 3 编译优化</div> 提升到渲染函数外,只有<div>{``{ msg }}</div>会在每次更新时重新处理。
- 树结构打平(Tree Flattening)
概念
Vue 3 会把嵌套的虚拟 DOM 树「打平」成一个数组结构,避免深度递归遍历。简单说:把多层嵌套的节点,转换成一维数组,记录每个节点的父子关系,diff 时只需遍历一维数组,无需递归,降低时间复杂度。
对比理解
- Vue 2 虚拟 DOM 结构(嵌套树):
js
// 嵌套结构,diff 时要递归遍历
const vnode = {
tag: 'div',
children: [
{ tag: 'ul', children: [
{ tag: 'li', children: [{ tag: 'span', children: '1' }] },
{ tag: 'li', children: [{ tag: 'span', children: '2' }] }
]}
]
}
- Vue 3 虚拟 DOM 结构(打平数组):
js
// 打平成数组,记录 parent/children 索引,无需递归
const vnodes = [
{ tag: 'div', children: [1] }, // 索引 0:div,子节点是索引 1
{ tag: 'ul', children: [2, 3] }, // 索引 1:ul,子节点是 2、3
{ tag: 'li', children: [4] }, // 索引 2:li,子节点是 4
{ tag: 'li', children: [5] }, // 索引 3:li,子节点是 5
{ tag: 'span', children: '1' }, // 索引 4:span
{ tag: 'span', children: '2' } // 索引 5:span
]
diff 时只需遍历这个一维数组,通过索引快速找到父子节点,效率远高于递归嵌套树。
- PatchFlags(补丁标记)
概念
Vue 3 在编译阶段给动态节点 打上「补丁标记」,标记该节点的「动态类型」(比如仅文本更新、仅 class 更新、仅属性更新等)。运行时 diff 时,只根据标记检查对应类型的变化,无需全量对比节点的所有属性。
核心 PatchFlags 枚举(常用)
| 标记值 | 含义 | 场景示例 |
| ------------ | ------------- | ----------------|
| 1 | TEXT |<div>{``{ msg }}</div>|
| 2 | CLASS |<div :class="cls"></div>|
| 4 | STYLE |<div :style="sty"></div>|
| 8 | PROPS |<div :id="id"></div>|
| 64 | FULL_PROPS |含动态 key 的属性(如 :[key]="val")|
| 128 | HYDRATE_EVENTS |绑定了事件的节点|
示例
模板:
vue
<template>
<div :class="boxCls" id="box">{{ content }}</div>
</template>
Vue 3 编译后,该节点会被打上 PatchFlags.TEXT | PatchFlags.CLASS(即 1 + 2 = 3)的标记。运行时更新时,只会检查:
- 文本内容(content)是否变化;
- class 属性(boxCls)是否变化;
- 完全忽略 id="box" 这个静态属性,也不会检查其他无关属性,精准更新。
- 缓存优化
Vue 3 对编译后的渲染函数、计算属性、事件处理等做了多层缓存,核心包括:
- 缓存渲染函数
编译后的渲染函数会被缓存,只有模板变化时才重新编译,避免重复解析模板。 - 缓存事件处理函数
对 @click="handleClick" 这类无参数的事件绑定,Vue 3 会缓存函数引用,避免每次渲染都创建新函数(解决 Vue 2 中 @click="() => handleClick(1)" 导致的不必要更新问题)。 - 缓存 vnode 创建
对于结构固定的动态节点(如 v-for 中固定结构的项),复用 vnode 结构,只更新动态数据。
示例:
vue
<template>
<!-- handleClick 会被缓存,不会每次渲染创建新函数 -->
<button @click="handleClick">点击</button>
<!-- 带参数的箭头函数,Vue 3 不会优化缓存,每次编译生成新的箭头函数 -->
<button @click="() => handleClick(1)">点击传参</button>
</template>
三、Vue 3 性能提升原因总结 + 完整示例代码
- 性能提升核心原因
- 编译阶段预处理:通过静态提升、PatchFlags 减少运行时无用计算;
- 虚拟 DOM 优化:树结构打平降低 diff 复杂度,从 O (n²) 趋近于 O (n);
- 缓存策略:复用静态节点、渲染函数、事件函数,减少重复创建 / 计算;
- 按需编译:只处理动态内容,静态内容全程复用。
总结
- Vue 3 编译优化的核心是「编译期预处理,运行期精准更新」:静态提升减少重复创建,PatchFlags 精准标记动态类型,树打平降低 diff 复杂度;
- 性能提升的关键:从「全量 diff 虚拟 DOM」变成「只处理动态内容」,静态内容全程复用;