Appearance
第 18 章 性能工程与最佳实践
本章要点
- Vue 3 的性能优化哲学:编译时优化 + 运行时精细控制的双轨策略
- 编译器的静态提升(Static Hoisting):将不变的 VNode 提取到渲染函数外部
- PatchFlag 与 Block Tree:精确追踪动态节点,跳过静态内容的 Diff
- 响应式系统的性能陷阱:深层响应式、大型数组、不必要的依赖收集
- shallowRef / shallowReactive / triggerRef 的精细控制策略
- 组件级优化:v-once、v-memo、defineAsyncComponent 与代码分割
- 虚拟列表与大数据渲染的底层实现原理
- 内存泄漏的检测与预防:闭包陷阱、全局事件、定时器清理
- DevTools Performance 面板:如何定位 Vue 应用的性能瓶颈
- 从源码角度理解每个优化手段的收益与代价
性能不是事后修补,而是架构决策。Vue 3 从设计之初就将性能作为核心目标——Proxy 替代 Object.defineProperty、编译器的静态分析、Block Tree 的精确 Diff、Tree-shaking 友好的模块化设计。这些底层改进让 Vue 3 在基准测试中全面领先 Vue 2。
但框架的性能上限只决定了地板,应用的实际表现取决于开发者如何使用它。本章将从源码层面剖析每个优化手段的原理,让你不仅知道"该怎么做",更理解"为什么这样做有效"。
18.1 编译时优化
静态提升(Static Hoisting)
Vue 3 编译器最重要的优化之一是静态提升——将不会变化的 VNode 创建操作提取到渲染函数外部,避免每次渲染都重复创建:
typescript
// 模板
// <div>
// <span class="title">固定标题</span>
// <span>{{ message }}</span>
// </div>
// ❌ 未优化:每次渲染都创建所有 VNode
function render(_ctx) {
return createVNode('div', null, [
createVNode('span', { class: 'title' }, '固定标题'),
createVNode('span', null, _ctx.message)
])
}
// ✅ 静态提升后:静态节点只创建一次
const _hoisted_1 = createVNode('span', { class: 'title' }, '固定标题')
function render(_ctx) {
return createVNode('div', null, [
_hoisted_1, // 直接复用,不重新创建
createVNode('span', null, _ctx.message)
])
}编译器是如何判断哪些节点可以提升的?
typescript
// compiler-core/src/transforms/hoistStatic.ts
function walk(
node: ParentNode,
context: TransformContext,
doNotHoistNode: boolean = false
) {
const { children } = node
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === NodeTypes.ELEMENT) {
// 计算节点的静态类型
const staticType = getConstantType(child, context)
if (staticType > ConstantTypes.NOT_CONSTANT) {
if (staticType >= ConstantTypes.CAN_HOIST) {
// 标记为可提升
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + ` /* HOISTED */`
// 将节点移到渲染函数外部
child.codegenNode = context.hoist(child.codegenNode!)
}
}
}
}
}
// 静态类型的分级
const enum ConstantTypes {
NOT_CONSTANT = 0, // 包含动态绑定
CAN_SKIP_PATCH = 1, // 可以跳过 Patch
CAN_HOIST = 2, // 可以提升到外部
CAN_STRINGIFY = 3 // 可以序列化为字符串(最高级别)
}静态字符串化
当连续的静态节点数量超过阈值(默认 20 个,或 5 个带属性的节点),编译器会将它们直接序列化为 HTML 字符串:
typescript
// 大量静态内容
// <div>
// <p>段落 1</p>
// <p>段落 2</p>
// ... (20+ 个静态段落)
// <p>段落 N</p>
// <p>{{ dynamic }}</p>
// </div>
// 编译结果:静态部分变成字符串
const _hoisted_1 = createStaticVNode(
'<p>段落 1</p><p>段落 2</p>...<p>段落 N</p>',
20 // 节点数量
)
function render(_ctx) {
return createVNode('div', null, [
_hoisted_1,
createVNode('p', null, _ctx.dynamic)
])
}createStaticVNode 的实现使用 innerHTML 一次性设置所有静态节点,比逐个 createElement 快得多:
typescript
function mountStaticNode(
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
namespace: ElementNamespace
) {
const nodes: RendererNode[] = []
// 使用 innerHTML 一次性创建所有节点
const template = document.createElement('template')
template.innerHTML = vnode.children as string
// 将所有子节点移到目标位置
const content = template.content
while (content.firstChild) {
nodes.push(content.firstChild)
container.insertBefore(content.firstChild, anchor)
}
// 记录首尾节点,用于后续移除
vnode.el = nodes[0]
vnode.anchor = nodes[nodes.length - 1]
}PatchFlag 精确追踪
PatchFlag 是 Vue 3 编译器最精巧的优化。它在编译时为每个动态节点标记"哪些部分是动态的",运行时只比较标记的部分:
typescript
// 编译器生成的 PatchFlag
const enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态非 class/style 的属性
FULL_PROPS = 1 << 4, // 有动态 key 的属性
NEED_HYDRATION = 1 << 5, // 需要 hydration 的事件监听
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, // 开发模式的根 Fragment
HOISTED = -1, // 静态提升的节点
BAIL = -2 // 放弃优化
}
// 模板:<div :class="cls" :style="stl" :id="id" @click="handler">
// 编译后:
createVNode('div', {
class: _ctx.cls,
style: _ctx.stl,
id: _ctx.id,
onClick: _ctx.handler
}, null,
PatchFlags.CLASS | PatchFlags.STYLE | PatchFlags.PROPS,
// ↑ 位运算组合:class + style + props
['id'] // 动态属性名列表(props 时需要)
)