Vue3 编译器优化:静态提升、补丁标记与 Block Tree 的实现原理

3764 字
19 分钟
Vue3 编译器优化:静态提升、补丁标记与 Block Tree 的实现原理

Vue3 编译器优化:静态提升、补丁标记与 Block Tree 的实现原理#

Vue3 之所以在运行时性能上相比 Vue2 有质的飞跃,核心秘密并不仅仅在于 Proxy 替代了 Object.defineProperty,更在于编译器层面做了大量的优化工作。Vue3 的模板编译器在编译阶段就能提取出大量静态信息,将这些信息以”提示”的形式传递给运行时,从而让虚拟 DOM 的 diff 过程跳过大量不必要的比较。

本文将深入分析 Vue3 编译器的三大核心优化策略:静态提升(hoistStatic)补丁标记(PatchFlag)Block Tree,并通过对比 Vue2 的实现,让你真正理解这些优化为何能带来数量级的性能提升。

一、Vue2 的 diff 困境#

在深入 Vue3 之前,我们先回顾 Vue2 的虚拟 DOM diff 过程存在的问题。

1.1 Vue2 的模板编译产物#

假设有如下模板:

<template>
<div>
<h1>Hello Vue</h1>
<p>这是一段静态文本</p>
<span>{{ message }}</span>
</div>
</template>

在 Vue2 中,编译后的 render 函数大致如下:

// Vue2 编译产物
function render() {
with (this) {
return _c('div', [
_c('h1', [_v("Hello Vue")]),
_c('p', [_v("这是一段静态文本")]),
_c('span', [_v(_s(message))])
])
}
}

每次组件重新渲染时,Vue2 会重新执行整个 render 函数,创建完整的 VNode 树,然后对新旧 VNode 树进行全量 diff。即使 <h1><p> 的内容永远不会变化,diff 算法依然要逐一比较它们的类型、属性和子节点。

1.2 全量 diff 的开销#

// Vue2 的 patch 过程(简化)
function patch(oldVNode, newVNode) {
// 即使是静态节点,也要走完整个比较流程
if (oldVNode.tag !== newVNode.tag) {
replaceNode(oldVNode, newVNode)
return
}
// 比较 props —— 静态节点也要比较
updateProps(oldVNode, newVNode)
// 比较 children —— 静态节点也要比较
updateChildren(oldVNode.children, newVNode.children)
}

在一个包含大量静态内容的页面中(比如文章详情页),可能 90% 的 DOM 节点都是静态的,但 Vue2 的 diff 算法不得不遍历它们全部。这就是 Vue2 在大型页面上性能瓶颈的根本原因。

二、PatchFlag:精准标记动态内容#

Vue3 编译器的第一个核心优化就是 PatchFlag(补丁标记)。编译器在编译阶段分析模板,为每个动态节点打上标记,告诉运行时”这个节点的哪些部分是动态的”。

2.1 PatchFlag 的定义#

packages/shared/src/patchFlags.ts
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态 props(不含 class/style)
FULL_PROPS = 1 << 4, // 有动态 key 的 props,需要完整 diff
HYDRATE_EVENTS = 1 << 5, // 有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 子节点顺序不会改变的 Fragment
KEYED_FRAGMENT = 1 << 7, // 子节点有 key 的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 需要非 props 的 patch(如 ref、指令)
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11, // 仅开发环境
HOISTED = -1, // 静态提升的节点
BAIL = -2 // 退出优化模式
}

PatchFlag 使用位运算来表示不同的动态类型,这使得多种标记可以组合使用,且判断效率极高。

2.2 编译产物对比#

同样的模板,看 Vue3 的编译产物:

<template>
<div>
<h1>Hello Vue</h1>
<p>这是一段静态文本</p>
<span :class="cls">{{ message }}</span>
</div>
</template>

Vue3 编译后:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "Hello Vue", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "这是一段静态文本", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_hoisted_2,
_createElementVNode("span", {
class: _normalizeClass(_ctx.cls)
}, _toDisplayString(_ctx.message), 3 /* TEXT, CLASS */)
]))
}

注意最后的 3 /* TEXT, CLASS */,这就是 PatchFlag。3 等于 1 | 2,即 TEXT | CLASS,表示这个节点只有文本内容和 class 是动态的。

2.3 运行时如何利用 PatchFlag#

// packages/runtime-core/src/renderer.ts(简化)
function patchElement(n1, n2, parentComponent) {
const el = (n2.el = n1.el!)
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
const { patchFlag } = n2
if (patchFlag > 0) {
// 有 PatchFlag,走快速路径
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class)
}
}
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style)
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
// ... 其他标记
} else if (!optimized) {
// 没有 PatchFlag,走全量 diff(兜底)
patchProps(el, n2, oldProps, newProps, parentComponent)
}
}

关键在于 if (patchFlag > 0) 这个判断。有 PatchFlag 的节点,运行时只会检查被标记的部分,完全跳过未标记的属性比较。对于上面的例子,运行时只需检查 class 和文本内容,不会浪费时间去比较其他 props。

2.4 位运算的妙用#

// 判断是否包含某个标记
if (patchFlag & PatchFlags.CLASS) {
// 有动态 class
}
// 组合多个标记
const flag = PatchFlags.TEXT | PatchFlags.CLASS // 3
// 判断是否是纯文本更新
if (patchFlag === PatchFlags.TEXT) {
// 只需更新文本,最快路径
}

位运算的判断只需一条 CPU 指令,比字符串比较或对象属性查找快得多。

三、静态提升(hoistStatic)#

3.1 什么是静态提升#

在上面的编译产物中,你可能已经注意到了 _hoisted_1_hoisted_2。它们被声明在 render 函数外部,这就是静态提升。

// 这两个 VNode 在模块加载时创建一次,之后每次渲染都复用
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "Hello Vue", -1)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "这是一段静态文本", -1)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1, // 直接引用,不重新创建
_hoisted_2, // 直接引用,不重新创建
// ... 动态节点
]))
}

3.2 提升的好处#

1. 避免重复创建 VNode 对象

没有提升时,每次渲染都要调用 createElementVNode 创建新的 VNode 对象,这意味着:

  • 内存分配(GC 压力)
  • 函数调用开销
  • 对象属性初始化

提升后,这些 VNode 只在模块初始化时创建一次。

2. diff 时直接跳过

function patch(n1, n2, container) {
// 如果新旧节点是同一个引用,直接跳过
if (n1 === n2) {
return
}
// ...
}

由于提升的节点在每次渲染中引用的是同一个对象(n1 === n2),diff 阶段会直接 return,不做任何处理。

3.3 静态提升的层级#

Vue3 的编译器不仅提升单个元素,还会提升静态子树

<template>
<div>
<section>
<h2>关于我们</h2>
<p>我们是一家科技公司</p>
<p>成立于 2020 年</p>
</section>
<span>{{ dynamicText }}</span>
</div>
</template>

编译后,整个 <section> 及其所有子节点会被作为一个整体提升:

const _hoisted_1 = /*#__PURE__*/_createStaticVNode(
"<section><h2>关于我们</h2><p>我们是一家科技公司</p><p>成立于 2020 年</p></section>",
1
)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("span", null, _toDisplayString(_ctx.dynamicText), 1 /* TEXT */)
]))
}

注意这里甚至用了 _createStaticVNode,直接将整个静态子树序列化为 HTML 字符串,在首次挂载时通过 innerHTML 一次性插入,连 VNode 的创建都省了。

3.4 静态属性提升#

不仅元素本身,静态的 props 对象也会被提升:

<template>
<div id="app" class="container">
<span :title="tip">{{ msg }}</span>
</div>
</template>
const _hoisted_1 = { id: "app", class: "container" }
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_createElementVNode("span", { title: _ctx.tip }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["title"])
]))
}

{ id: "app", class: "container" } 这个 props 对象被提升到模块顶层,避免每次渲染重新创建对象字面量。

四、Block Tree:革命性的 diff 策略#

Block Tree 是 Vue3 编译器优化中最精妙的设计,它从根本上改变了虚拟 DOM 的 diff 策略。

4.1 传统 diff 的问题#

传统的虚拟 DOM diff 是逐层递归的,即使使用了 PatchFlag,仍然需要遍历整个 VNode 树来找到动态节点:

div
├── h1 (静态) ← 仍然需要访问
├── section (静态)
│ ├── p (静态) ← 仍然需要访问
│ └── p (静态) ← 仍然需要访问
├── p (静态) ← 仍然需要访问
└── span (动态) ← 实际需要更新的只有这个

如果页面有 1000 个节点,但只有 5 个是动态的,传统 diff 仍然要遍历 1000 个节点。

4.2 Block 的概念#

Vue3 引入了 Block 的概念。一个 Block 是一个特殊的 VNode,它会收集其子树中所有的动态节点到一个扁平数组 dynamicChildren 中。

// VNode 结构(简化)
interface VNode {
type: string | Component
props: Record<string, any> | null
children: VNodeChild
patchFlag: number
dynamicChildren: VNode[] | null // Block 独有!
}

4.3 openBlock 和 createBlock 的工作原理#

packages/runtime-core/src/vnode.ts
// 当前正在收集动态节点的 Block 栈
const blockStack: VNode[][] = []
let currentBlock: VNode[] | null = null
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
export function createElementBlock(
type: string,
props?: Record<string, any>,
children?: unknown,
patchFlag?: number
) {
return setupBlock(
createBaseVNode(type, props, children, patchFlag)
)
}
function setupBlock(vnode: VNode) {
// 将收集到的动态节点数组挂到 Block 节点上
vnode.dynamicChildren = currentBlock || EMPTY_ARR
// 弹出当前 Block
blockStack.pop()
// 恢复上一层 Block
currentBlock = blockStack[blockStack.length - 1] || null
// 如果还有上层 Block,当前 Block 本身也是上层的动态节点
if (currentBlock) {
currentBlock.push(vnode)
}
return vnode
}

当创建动态 VNode 时:

export function createElementVNode(
type: string,
props?: any,
children?: unknown,
patchFlag?: number
) {
const vnode = createBaseVNode(type, props, children, patchFlag)
// 关键:如果有 patchFlag 且当前正在收集,则加入 dynamicChildren
if (patchFlag > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}

4.4 Block Tree 的 diff 过程#

function patchElement(n1, n2) {
// ... props 更新
if (n2.dynamicChildren) {
// 这是一个 Block!只 diff dynamicChildren
patchBlockChildren(n1.dynamicChildren!, n2.dynamicChildren)
} else {
// 不是 Block,走传统的全量 diff
patchChildren(n1, n2, el)
}
}
function patchBlockChildren(
oldChildren: VNode[],
newChildren: VNode[]
) {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// 直接 patch 动态节点,跳过所有静态节点
patch(oldVNode, newVNode, ...)
}
}

看到了吗?patchBlockChildren 直接遍历 dynamicChildren 数组,完全跳过了静态节点。这是一个扁平化的遍历,而不是递归的树遍历。

4.5 实际效果#

<template>
<div>
<h1>标题</h1>
<p>段落 1</p>
<p>段落 2</p>
<!-- ... 假设有 100 个静态段落 ... -->
<p>段落 100</p>
<span>{{ message }}</span>
<button @click="handleClick" :disabled="loading">
{{ buttonText }}
</button>
</div>
</template>

在 Vue2 中,每次更新需要遍历 103 个节点。

在 Vue3 中,Block 的 dynamicChildren 只包含 2 个节点(<span><button>),diff 过程只需要处理这 2 个节点。从 O(n) 降低到 O(动态节点数)

4.6 结构化指令与 Block#

v-ifv-for 等结构化指令会创建新的 Block,因为它们会改变子树的结构:

<template>
<div>
<p v-if="show">条件内容</p>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
<span>{{ msg }}</span>
</div>
</template>

编译后:

export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
// v-if 创建自己的 Block
_ctx.show
? (_openBlock(), _createElementBlock("p", { key: 0 }, "条件内容"))
: _createCommentVNode("v-if", true),
_createElementVNode("ul", null, [
// v-for 创建自己的 Block(Fragment)
(_openBlock(true), _createElementBlock(_Fragment, null,
_renderList(_ctx.list, (item) => {
return (_openBlock(), _createElementBlock("li", {
key: item.id
}, _toDisplayString(item.name), 1 /* TEXT */))
}),
128 /* KEYED_FRAGMENT */
))
]),
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}

每个 v-ifv-for 都有自己的 openBlock/createBlock,形成一个 Block Tree(Block 的树形结构)。根 Block 的 dynamicChildren 中包含子 Block,更新时先定位到具体的 Block,再处理其内部的动态节点。

五、缓存事件处理函数#

除了上述三大优化,Vue3 编译器还会缓存内联事件处理函数

<template>
<button @click="count++">{{ count }}</button>
</template>

编译后:

export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("button", {
onClick: _cache[0] || (_cache[0] = ($event) => (_ctx.count++))
}, _toDisplayString(_ctx.count), 1 /* TEXT */))
}

注意 _cache[0] || (_cache[0] = ...),事件处理函数只创建一次,后续渲染直接从缓存读取。这避免了因为 onClick 引用变化导致的不必要的子组件更新。

如果没有缓存,每次渲染都会创建新的箭头函数,导致 props.onClick 引用变化,子组件会认为 props 发生了变化而触发重新渲染。

六、编译优化的完整流程#

让我们用一个完整的例子串联所有优化:

<template>
<div class="page">
<header>
<h1>我的应用</h1>
<nav>
<a href="/home">首页</a>
<a href="/about">关于</a>
</nav>
</header>
<main>
<article v-for="post in posts" :key="post.id">
<h2>{{ post.title }}</h2>
<p :class="{ highlight: post.featured }">{{ post.summary }}</p>
<button @click="like(post)">👍 {{ post.likes }}</button>
</article>
</main>
<footer>
<p>© 2026 My App</p>
</footer>
</div>
</template>

Vue3 编译器的处理过程:

// 1. 静态提升:header 和 footer 整体提升
const _hoisted_1 = /*#__PURE__*/_createStaticVNode(
'<header><h1>我的应用</h1><nav><a href="/home">首页</a><a href="/about">关于</a></nav></header>',
1
)
const _hoisted_2 = /*#__PURE__*/_createStaticVNode(
'<footer><p>© 2026 My App</p></footer>',
1
)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", { class: "page" }, [
_hoisted_1, // 静态提升,diff 时跳过
_createElementVNode("main", null, [
// 2. v-for 创建 Block Fragment
(_openBlock(true), _createElementBlock(_Fragment, null,
_renderList(_ctx.posts, (post) => {
return (_openBlock(), _createElementBlock("article", {
key: post.id
}, [
// 3. PatchFlag: TEXT
_createElementVNode("h2", null, _toDisplayString(post.title), 1),
// 4. PatchFlag: TEXT | CLASS
_createElementVNode("p", {
class: _normalizeClass({ highlight: post.featured })
}, _toDisplayString(post.summary), 3),
// 5. 事件缓存 + PatchFlag: TEXT
_createElementVNode("button", {
onClick: ($event) => (_ctx.like(post))
}, "👍 " + _toDisplayString(post.likes), 9, ["onClick"])
]))
}),
128 /* KEYED_FRAGMENT */
))
]),
_hoisted_2 // 静态提升,diff 时跳过
]))
}

更新时的 diff 路径:

  1. 根 Block 的 dynamicChildren 只包含 v-for 的 Fragment Block
  2. Fragment Block 对列表项做 keyed diff
  3. 每个列表项 Block 的 dynamicChildren 只包含 3 个动态节点
  4. 对每个动态节点,根据 PatchFlag 只更新标记的部分

整个 <header>(5 个 DOM 节点)和 <footer>(2 个 DOM 节点)完全不参与 diff。

七、性能对比数据#

根据 Vue 团队的基准测试数据,在典型场景下:

场景Vue2 耗时Vue3 耗时提升
大量静态内容的页面更新36ms5ms~7x
列表项局部更新22ms8ms~2.7x
深层嵌套组件更新45ms12ms~3.7x
事件处理函数导致的子组件更新18ms3ms~6x

这些数据说明,编译器优化带来的提升是实实在在的,尤其在静态内容占比高的页面上效果最为显著。

八、如何在 Vue SFC Playground 中查看编译产物#

你可以在 Vue SFC Playground 中实时查看任意模板的编译产物:

  1. 在左侧编辑器写入模板代码
  2. 点击右上角的 “JS” 或 “Output” 选项卡
  3. 勾选 “SSR”、“hoistStatic” 等选项查看不同编译结果

你也可以通过 @vue/compiler-sfc 编程式编译:

import { compile } from '@vue/compiler-dom'
const { code } = compile(`
<div>
<h1>Static</h1>
<span>{{ dynamic }}</span>
</div>
`, {
mode: 'module',
hoistStatic: true,
prefixIdentifiers: true
})
console.log(code)

九、总结#

Vue3 的编译器优化体系可以用一句话概括:在编译时做尽可能多的工作,减少运行时的负担

  • PatchFlag 让运行时知道每个节点”哪些部分是动态的”,避免不必要的属性比较
  • 静态提升 让静态内容只创建一次,后续渲染直接复用,diff 时通过引用相等直接跳过
  • Block Tree 将动态节点收集到扁平数组中,将 diff 的复杂度从”与模板大小成正比”降低为”与动态内容数量成正比”
  • 事件缓存 避免因为内联事件处理函数引用变化导致的不必要更新

这些优化是自动的,你不需要做任何额外的工作,只要使用模板语法,编译器就会自动应用这些优化。这也是 Vue 团队推荐使用模板而非 JSX 的原因之一 —— 模板的约束性更强,编译器能提取更多的静态信息。

理解这些底层原理,不仅能帮助你写出更高性能的 Vue3 代码,还能在遇到性能问题时有更清晰的排查思路。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Vue3 编译器优化:静态提升、补丁标记与 Block Tree 的实现原理
https://boke.hackerdream.xyz/posts/vue3-compiler-optimization/
作者
晴天
发布于
2026-03-04
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录