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 时需要)
)运行时如何利用 PatchFlag:
typescript
// runtime-core/src/renderer.ts - patchElement
function patchElement(
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
// ...
) {
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.FULL_PROPS) {
// 动态 key,需要全量 diff props
patchProps(el, n2, oldProps, newProps, parentComponent, ...)
} else {
// 按位检查,只更新变化的部分
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.PROPS) {
// 只检查声明的动态属性
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
if (next !== prev || key === 'value') {
hostPatchProp(el, key, prev, next, ...)
}
}
}
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, ...)
}
}PatchFlag 的威力在于将 O(n) 的属性比较降低为 O(1) 的位运算检查。一个有 10 个属性但只有 1 个是动态的元素,传统 Diff 需要比较 10 次,PatchFlag 只需比较 1 次。
Block Tree 与动态节点收集
Block Tree 是配合 PatchFlag 使用的更高层次优化。它将组件的 VNode 树"拍平",直接追踪所有动态节点:
typescript
// Block 的创建
export function openBlock(disableTracking = false) {
blockStack.push(
(currentBlock = disableTracking ? null : [])
)
}
export function createBlock(
type: VNodeTypes,
props?: Record<string, any> | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[]
): VNode {
return setupBlock(
createVNode(type, props, children, patchFlag, dynamicProps, true)
)
}
function setupBlock(vnode: VNode): VNode {
// 将收集到的动态节点附加到 Block 根节点
vnode.dynamicChildren = currentBlock || EMPTY_ARR
// 关闭当前 Block
closeBlock()
// 如果有父 Block,将当前节点注册为父 Block 的动态子节点
if (currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
// 每个动态节点在创建时自动注册到当前 Block
export function createVNode(/* ... */): VNode {
// ...
if (
currentBlock &&
vnode.patchFlag !== PatchFlags.HOISTED &&
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT)
) {
// 将动态节点加入当前 Block 的收集列表
currentBlock.push(vnode)
}
return vnode
}Patch 阶段利用 dynamicChildren 直接跳过静态子树:
typescript
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, /* ... */)
}
}18.2 响应式系统的性能优化
避免不必要的深层响应
reactive() 默认创建深层响应式代理——对象的每一层嵌套都会被代理。对于大型、层次很深的数据结构,这是巨大的开销:
typescript
// ❌ 性能隐患:10000 个对象每个都被深度代理
const state = reactive({
items: generateItems(10000) // 每个 item 有 10+ 个嵌套属性
})
// 源码中的深层代理逻辑
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
track(target, TrackOpTypes.GET, key)
if (isObject(res)) {
// 关键:访问嵌套对象时,递归代理
return isReadonly ? readonly(res) : reactive(res)
// 每次访问都会检查/创建代理,虽然有缓存,但仍有开销
}
return res
}
}
// ✅ 优化:使用 shallowRef 或 shallowReactive
const state = shallowReactive({
items: generateItems(10000) // 只有 items 属性是响应式的,内部不代理
})
// 需要更新时:
function updateItem(index: number, newData: Partial<Item>) {
// 替换整个数组元素,触发浅层响应
state.items[index] = { ...state.items[index], ...newData }
// 或者替换整个数组
state.items = [...state.items]
}