前言
很多人知道 Vue3 比 Vue2 快,但被问到**"为什么快"**时,回答往往是:
"因为用了 Proxy。"
这个回答只对了一半。
Proxy 确实让响应式 更快了,但 Vue3 的渲染性能 提升,更多来自编译时的优化。
Vue3 的模板编译器在编译阶段做了大量工作:
-
✅ PatchFlags(补丁标记):让 Diff 只比对动态内容
-
✅ 静态提升(Static Hoisting):跳过不变的节点
-
✅ 事件缓存(Handler Caching):避免不必要的子组件更新
-
✅ Tree Shaking:只打包你用到的代码
这一篇,我们逐一拆解这些"黑盒"机制。
一、Vue2 的 Diff 为什么慢?
1️⃣ Vue2 的 Diff 策略
<template>
<div>
<h1>标题</h1>
<p>{{ count }}</p>
<span>固定的文字</span>
</div>
</template>
当 count变化时,Vue2 会做什么?
1. 对比 div → 相同
2. 对比 h1 → 相同(但还是要对比)
3. 对比 p → 内容变了,更新
4. 对比 span → 相同(但还是要对比)
❌ 即使内容永远不会变,也要参与 Diff
2️⃣ 问题本质
Vue2 在运行时不知道哪些是动态的、哪些是静态的。
它只能暴力地递归对比整棵 VNode 树。
二、PatchFlags:Vue3 的"精准打击"
1️⃣ 核心思想
在编译阶段就标记出"哪些内容是动态的",运行时只比对这些标记。
2️⃣ 编译产物对比
Vue3 模板
<template>
<div>
<h1>标题</h1>
<p>{{ count }}</p>
<span>固定的文字</span>
</div>
</template>
编译后的渲染函数(简化版)
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("h1", null, "标题"),
_createElementVNode("p", null, _toDisplayString(_ctx.count), 1 /* TEXT */),
_createElementVNode("span", null, "固定的文字")
]))
}
📌 注意 1 /* TEXT */这个标记
3️⃣ PatchFlags 枚举值
export const enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 2, // 动态类名
STYLE = 4, // 动态样式
PROPS = 8, // 动态属性(不含 class/style)
FULL_PROPS = 16, // 有动态 key 的属性
HYDRATE_EVENTS = 32, // 事件监听
STABLE_FRAGMENT = 64, // 子节点顺序稳定
KEYED_FRAGMENT = 128, // 带 key 的片段
UNKEYED_FRAGMENT = 256, // 不带 key 的片段
NEED_PATCH = 512, // 需要非 props 补丁
DYNAMIC_SLOTS = 1024, // 动态插槽
HOISTED = -1, // 静态节点(已提升)
BAIL = -2 // Diff 退化为全量比对
}
4️⃣ Diff 时的精准比对
count 变化
→ 运行时看到 p 节点有 TEXT 标记
→ 只比对 p 的文本内容
→ h1 和 span 没有标记,直接跳过
✅ Diff 时间复杂度从 O(n) 降到了接近 O(1)
三、静态提升(Static Hoisting)
1️⃣ 问题
<template>
<div>
<h1>标题</h1>
<p>{{ count }}</p>
</div>
</template>
每次渲染都要创建 h1的 VNode:
_createElementVNode("h1", null, "标题") // 每次都执行
2️⃣ Vue3 的优化
编译后:
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "标题", -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("p", null, _toDisplayString(_ctx.count), 1 /* TEXT */)
]))
}
📌 _hoisted_1只创建一次,后续渲染直接复用
3️⃣ 效果
| 优化前 | 优化后 |
|---|---|
| 每次渲染都创建静态 VNode | 只创建一次 |
| 参与 Diff | 标记为 HOISTED,跳过 Diff |
| 内存分配频繁 | 内存复用 |
四、事件缓存(Handler Caching)
1️⃣ Vue2 的问题
<template>
<button @click="handleClick">点击</button>
</template>
每次渲染都会创建一个新的函数:
// 每次渲染都执行
h('button', { onClick: () => handleClick() })
👉 父组件更新 → 子组件 props 变化 → 子组件重新渲染
2️⃣ Vue3 的优化
const _cache = {}
// 第一次渲染
_cache[0] = ($event) => _ctx.handleClick($event)
// 后续渲染
// 直接从缓存中取,函数引用不变
📌 效果:
-
事件处理函数引用稳定
-
不会触发子组件不必要的更新
五、Tree Shaking:只打包用到的代码
1️⃣ Vue2 的问题
import Vue from 'vue'
// 即使只用了一个功能,也要打包整个 Vue
📌 Vue2 的 API 都在一个对象上,无法被 Tree Shake
2️⃣ Vue3 的模块化设计
import { ref, computed, watch } from 'vue'
// 只打包这三个函数
📌 Vue3 的所有 API 都是独立导出的
3️⃣ 实际效果对比
| 项目 | Vue2 | Vue3 |
|---|---|---|
| 最小体积(gzipped) | ~23 KB | ~13 KB |
| 只用一个 API | 打包全部 | 只打包该 API |
六、综合优化效果演示
模板
<template>
<div class="container" :class="theme">
<header>
<h1>Logo</h1>
<nav>
<a href="#">首页</a>
<a href="#">关于</a>
</nav>
</header>
<main>
<p>{{ message }}</p>
<button @click="update">更新</button>
</main>
</div>
</template>
编译优化分析
| 节点 | 优化方式 |
|---|---|
header及其内容 |
静态提升,不参与 Diff |
nav内的链接 |
静态提升 |
class="container" |
有动态绑定,标记 CLASS |
{``{ message }} |
标记 TEXT |
@click |
事件缓存 |
📌 实际参与 Diff 的只有两个动态节点
七、如何验证这些优化?
1️⃣ 查看编译产物
# 在 Vite 项目中
npx vite build
cat dist/assets/*.js
2️⃣ Vue DevTools
-
观察组件的渲染次数
-
对比优化前后的渲染耗时
八、面试高频问答
Q1:PatchFlags 是什么?
编译阶段标记动态节点的类型,运行时只 Diff 有标记的节点。
Q2:静态提升有什么好处?
避免重复创建静态 VNode,跳过 Diff,减少 GC 压力。
Q3:Vue3 为什么能 Tree Shaking?
因为 API 是独立导出的,不是挂在单一对象上。
九、总结(原理级)
Vue3 的渲染优化是一个三层架构:
| 层级 | 优化手段 | 效果 |
|---|---|---|
| 编译时 | PatchFlags | 精准 Diff |
| 编译时 | 静态提升 | 跳过不变的节点 |
| 运行时 | 事件缓存 | 稳定的函数引用 |
| 构建时 | Tree Shaking | 更小的包体积 |
Vue3 的快,不是"运行时更快",而是"尽量少做无用功"。
📢 下期预告
👉 第 23 篇:自定义指令(Directives)------ 封装你的 DOM 操作利器