Skip to content

第 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]
}

基于 VitePress 构建