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 时需要)
)

基于 VitePress 构建