《Vue3 从入门到大神22篇》渲染优化 —— PatchFlags 与 Tree Shaking 的黑盒解析

前言

很多人知道 Vue3 比 Vue2 快,但被问到**"为什么快"**时,回答往往是:

"因为用了 Proxy。"

这个回答只对了一半。

Proxy 确实让响应式 更快了,但 Vue3 的渲染性能 提升,更多来自编译时的优化

Vue3 的模板编译器在编译阶段做了大量工作:

  • PatchFlags(补丁标记):让 Diff 只比对动态内容

  • 静态提升(Static Hoisting):跳过不变的节点

  • 事件缓存(Handler Caching):避免不必要的子组件更新

  • Tree Shaking:只打包你用到的代码

这一篇,我们逐一拆解这些"黑盒"机制。


一、Vue2 的 Diff 为什么慢?

1️⃣ Vue2 的 Diff 策略

复制代码
<template>
  <div>
    <h1>标题</h1>
    <p>{{ count }}</p>
    <span>固定的文字</span>
  </div>
</template>

count变化时,Vue2 会做什么?

复制代码
1. 对比 div → 相同
2. 对比 h1 → 相同(但还是要对比)
3. 对比 p → 内容变了,更新
4. 对比 span → 相同(但还是要对比)

即使内容永远不会变,也要参与 Diff


2️⃣ 问题本质

Vue2 在运行时不知道哪些是动态的、哪些是静态的

它只能暴力地递归对比整棵 VNode 树。


二、PatchFlags:Vue3 的"精准打击"

1️⃣ 核心思想

在编译阶段就标记出"哪些内容是动态的",运行时只比对这些标记。


2️⃣ 编译产物对比

Vue3 模板
复制代码
<template>
  <div>
    <h1>标题</h1>
    <p>{{ count }}</p>
    <span>固定的文字</span>
  </div>
</template>
编译后的渲染函数(简化版)
复制代码
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("h1", null, "标题"),
    _createElementVNode("p", null, _toDisplayString(_ctx.count), 1 /* TEXT */),
    _createElementVNode("span", null, "固定的文字")
  ]))
}

📌 注意 1 /* TEXT */这个标记


3️⃣ PatchFlags 枚举值

复制代码
export const enum PatchFlags {
  TEXT = 1,        // 动态文本
  CLASS = 2,       // 动态类名
  STYLE = 4,       // 动态样式
  PROPS = 8,       // 动态属性(不含 class/style)
  FULL_PROPS = 16, // 有动态 key 的属性
  HYDRATE_EVENTS = 32, // 事件监听
  STABLE_FRAGMENT = 64, // 子节点顺序稳定
  KEYED_FRAGMENT = 128, // 带 key 的片段
  UNKEYED_FRAGMENT = 256, // 不带 key 的片段
  NEED_PATCH = 512, // 需要非 props 补丁
  DYNAMIC_SLOTS = 1024, // 动态插槽
  HOISTED = -1,     // 静态节点(已提升)
  BAIL = -2         // Diff 退化为全量比对
}

4️⃣ Diff 时的精准比对

复制代码
count 变化
  → 运行时看到 p 节点有 TEXT 标记
  → 只比对 p 的文本内容
  → h1 和 span 没有标记,直接跳过

Diff 时间复杂度从 O(n) 降到了接近 O(1)


三、静态提升(Static Hoisting)

1️⃣ 问题

复制代码
<template>
  <div>
    <h1>标题</h1>
    <p>{{ count }}</p>
  </div>
</template>

每次渲染都要创建 h1的 VNode:

复制代码
_createElementVNode("h1", null, "标题") // 每次都执行

2️⃣ Vue3 的优化

编译后:

复制代码
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "标题", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("p", null, _toDisplayString(_ctx.count), 1 /* TEXT */)
  ]))
}

📌 _hoisted_1只创建一次,后续渲染直接复用


3️⃣ 效果

优化前 优化后
每次渲染都创建静态 VNode 只创建一次
参与 Diff 标记为 HOISTED,跳过 Diff
内存分配频繁 内存复用

四、事件缓存(Handler Caching)

1️⃣ Vue2 的问题

复制代码
<template>
  <button @click="handleClick">点击</button>
</template>

每次渲染都会创建一个新的函数:

复制代码
// 每次渲染都执行
h('button', { onClick: () => handleClick() })

👉 父组件更新 → 子组件 props 变化 → 子组件重新渲染


2️⃣ Vue3 的优化

复制代码
const _cache = {}

// 第一次渲染
_cache[0] = ($event) => _ctx.handleClick($event)

// 后续渲染
// 直接从缓存中取,函数引用不变

📌 效果

  • 事件处理函数引用稳定

  • 不会触发子组件不必要的更新


五、Tree Shaking:只打包用到的代码

1️⃣ Vue2 的问题

复制代码
import Vue from 'vue'
// 即使只用了一个功能,也要打包整个 Vue

📌 Vue2 的 API 都在一个对象上,无法被 Tree Shake


2️⃣ Vue3 的模块化设计

复制代码
import { ref, computed, watch } from 'vue'
// 只打包这三个函数

📌 Vue3 的所有 API 都是独立导出的


3️⃣ 实际效果对比

项目 Vue2 Vue3
最小体积(gzipped) ~23 KB ~13 KB
只用一个 API 打包全部 只打包该 API

六、综合优化效果演示

模板

复制代码
<template>
  <div class="container" :class="theme">
    <header>
      <h1>Logo</h1>
      <nav>
        <a href="#">首页</a>
        <a href="#">关于</a>
      </nav>
    </header>
    <main>
      <p>{{ message }}</p>
      <button @click="update">更新</button>
    </main>
  </div>
</template>

编译优化分析

节点 优化方式
header及其内容 静态提升,不参与 Diff
nav内的链接 静态提升
class="container" 有动态绑定,标记 CLASS
{``{ message }} 标记 TEXT
@click 事件缓存

📌 实际参与 Diff 的只有两个动态节点


七、如何验证这些优化?

1️⃣ 查看编译产物

复制代码
# 在 Vite 项目中
npx vite build
cat dist/assets/*.js

2️⃣ Vue DevTools

  • 观察组件的渲染次数

  • 对比优化前后的渲染耗时


八、面试高频问答

Q1:PatchFlags 是什么?

编译阶段标记动态节点的类型,运行时只 Diff 有标记的节点。

Q2:静态提升有什么好处?

避免重复创建静态 VNode,跳过 Diff,减少 GC 压力。

Q3:Vue3 为什么能 Tree Shaking?

因为 API 是独立导出的,不是挂在单一对象上。


九、总结(原理级)

Vue3 的渲染优化是一个三层架构

层级 优化手段 效果
编译时 PatchFlags 精准 Diff
编译时 静态提升 跳过不变的节点
运行时 事件缓存 稳定的函数引用
构建时 Tree Shaking 更小的包体积

Vue3 的快,不是"运行时更快",而是"尽量少做无用功"。


📢 下期预告

👉 第 23 篇:自定义指令(Directives)------ 封装你的 DOM 操作利器