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 的定义
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 的工作原理
// 当前正在收集动态节点的 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-if、v-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-if 和 v-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 路径:
- 根 Block 的
dynamicChildren只包含v-for的 Fragment Block - Fragment Block 对列表项做 keyed diff
- 每个列表项 Block 的
dynamicChildren只包含 3 个动态节点 - 对每个动态节点,根据 PatchFlag 只更新标记的部分
整个 <header>(5 个 DOM 节点)和 <footer>(2 个 DOM 节点)完全不参与 diff。
七、性能对比数据
根据 Vue 团队的基准测试数据,在典型场景下:
| 场景 | Vue2 耗时 | Vue3 耗时 | 提升 |
|---|---|---|---|
| 大量静态内容的页面更新 | 36ms | 5ms | ~7x |
| 列表项局部更新 | 22ms | 8ms | ~2.7x |
| 深层嵌套组件更新 | 45ms | 12ms | ~3.7x |
| 事件处理函数导致的子组件更新 | 18ms | 3ms | ~6x |
这些数据说明,编译器优化带来的提升是实实在在的,尤其在静态内容占比高的页面上效果最为显著。
八、如何在 Vue SFC Playground 中查看编译产物
你可以在 Vue SFC Playground 中实时查看任意模板的编译产物:
- 在左侧编辑器写入模板代码
- 点击右上角的 “JS” 或 “Output” 选项卡
- 勾选 “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 代码,还能在遇到性能问题时有更清晰的排查思路。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!