跳到正文
前端知识库
前端框架

Vue3.0 性能提升主要是通过哪几方面体现的?

Vue3 在性能方面做了大量优化。面试考察这个问题,是想要了解候选人对 Vue 底层实现原理的理解,以及能否从编译阶段、源码体积、响应式系统三个维度分析性能提升。

9 分钟

Vue3.0 性能提升主要是通过哪几方面体现的?

问题解析

Vue3 在性能方面做了大量优化。面试考察这个问题,是想要了解候选人对 Vue 底层实现原理的理解,以及能否从编译阶段、源码体积、响应式系统三个维度分析性能提升。

核心概念

Vue3 性能优化全景

┌─────────────────────────────────────────────────────┐
│                 Vue3 性能优化                        │
├───────────────┬───────────────┬─────────────────────┤
│   编译阶段    │   源码体积    │     响应式系统      │
├───────────────┼───────────────┼─────────────────────┤
│ • 静态标记    │ • Tree        │ • Proxy 替代        │
│ • 静态提升    │   Shaking     │   defineProperty    │
│ • 事件缓存    │ • 移除无用API │ • 懒加载递归        │
│ • SSR优化     │ • 运行时更小  │ • 自动监听新增/删除 │
└───────────────┴───────────────┴─────────────────────┘

详细解答

一、编译阶段优化

1. Diff 算法优化 - PatchFlag

Vue2 的问题:

<template>
  <div id="content">
    <p>静态文本1</p>
    <p>静态文本2</p>
    <p>{{ message }}</p>  <!-- 只有这一个是动态的 -->
    <p>静态文本3</p>
    <p>静态文本4</p>
    <!-- ... 更多静态节点 -->
  </div>
</template>
// Vue2: Diff 时需要遍历所有节点
function patch(oldVNode, newVNode) {
  // 对比所有节点,包括静态节点
  for (let i = 0; i < children.length; i++) {
    patchChild(children[i]);  // 全部对比,性能浪费
  }
}

Vue3 的优化:

// 编译时给动态节点添加标记(PatchFlag)
export const enum PatchFlags {
  TEXT = 1,              // 动态文本节点
  CLASS = 1 << 1,        // 动态 class
  STYLE = 1 << 2,        // 动态 style
  PROPS = 1 << 3,        // 动态属性(不包括 class/style)
  FULL_PROPS = 1 << 4,   // 动态 key,需要完整 diff
  HYDRATE_EVENTS = 1 << 5,  // 带有事件监听
  STABLE_FRAGMENT = 1 << 6, // 子节点顺序不变的 Fragment
  KEYED_FRAGMENT = 1 << 7,  // 带 key 的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 不带 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 需要 patch 的
  DYNAMIC_SLOTS = 1 << 10,  // 动态插槽
  HOISTED = -1,          // 静态节点,跳过 diff
  BAIL = -2              // 退出优化模式
}
<template>
  <div>
    <p>静态文本</p>                    <!-- HOISTED (-1) -->
    <p>{{ message }}</p>              <!-- TEXT (1) -->
    <p :class="cls">内容</p>          <!-- CLASS (2) -->
    <p :style="styleObj">内容</p>     <!-- STYLE (4) -->
    <p :id="id" :title="title">内容</p> <!-- PROPS (8) -->
  </div>
</template>
// 编译后的代码
function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,                                          // 静态节点,直接复用
    _createVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
    _createVNode("p", { class: _ctx.cls }, null, 2 /* CLASS */),
    _createVNode("p", { style: _ctx.styleObj }, null, 4 /* STYLE */),
    _createVNode("p", { id: _ctx.id, title: _ctx.title }, null, 8 /* PROPS */, ["id", "title"])
  ]));
}

Diff 时只对比标记的节点:

const patchElement = (n1, n2) => {
  const { patchFlag, dynamicChildren } = n2;

  // 1. 如果 patchFlag > 0,只对比标记的属性
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.FULL_PROPS) {
      // 动态 key,完整对比所有 props
      patchProps(el, n2, oldProps, newProps);
    } 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);
      }
      // ... 其他标记
    }
  }

  // 2. 如果 patchFlag === -1 (HOISTED),完全跳过
  // 3. 如果有 dynamicChildren,直接对比动态子节点(Block Tree)
  if (dynamicChildren) {
    patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren);
  }
};

2. 静态提升(Hoist Static)

<template>
  <div>
    <span>你好</span>
    <div>{{ message }}</div>
  </div>
</template>
// 优化前(Vue2 风格):每次渲染都创建静态节点
function render(_ctx, _cache) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("span", null, "你好"),    // 每次重新创建
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */));
}

// 优化后(Vue3):静态节点提升到 render 函数外
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */);

function render(_ctx, _cache) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _hoisted_1,  // 直接复用,不重新创建
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */));
}

优化效果:

  • 静态节点只在初始化时创建一次
  • 每次渲染直接复用,减少内存分配
  • 不会被加入 Diff 比较

3. 事件监听缓存

<template>
  <div>
    <button @click="onClick">点我</button>
  </div>
</template>
// 未开启缓存:每次都被视为动态绑定
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _ctx.onClick  // 每次都会追踪变化
    }, "点我", 8 /* PROPS */, ["onClick"])
  ]));
});

// 开启缓存后:只在首次绑定
export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => _ctx.onClick(...args))
      // 使用缓存,无 PatchFlag
    }, "点我")
  ]));
}

4. Block Tree - 树结构打平

<template>
  <div>  <!-- Block Root -->
    <p>静态1</p>
    <p>{{ dynamic1 }}</p>  <!-- 动态节点1 -->
    <div>  <!-- Block -->
      <span>静态2</span>
      <span>{{ dynamic2 }}</span>  <!-- 动态节点2 -->
    </div>
    <p>{{ dynamic3 }}</p>  <!-- 动态节点3 -->
  </div>
</template>
// 传统 Diff:需要递归遍历整棵树 O(n)
// Block Tree:只收集动态节点,打平遍历 O(1) ~ O(k) k为动态节点数

function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "静态1"),
    _createVNode("p", null, _ctx.dynamic1, 1 /* TEXT */),
    (_openBlock(), _createBlock("div", null, [
      _createVNode("span", null, "静态2"),
      _createVNode("span", null, _ctx.dynamic2, 1 /* TEXT */)
    ])),
    _createVNode("p", null, _ctx.dynamic3, 1 /* TEXT */)
  ]));
}

// 实际生成的 Block Tree 结构
const block = {
  children: [
    /* 0: 静态节点,不收集 */
    /* 1: */ [VNode, dynamic1],
    /* 2: Block */
    /* 3: */ [VNode, dynamic3]
  ],
  dynamicChildren: [
    /* 0: */ [VNode, dynamic1],
    /* 1: */ [Block, { dynamicChildren: [[VNode, dynamic2]] }],
    /* 2: */ [VNode, dynamic3]
  ]
};

5. SSR 优化

<template>
  <div>
    <span>你好</span>
    <span>{{ message }}</span>
    <!-- 大量静态内容... -->
  </div>
</template>
// 优化前:创建 VNode -> 序列化 -> 生成 HTML
// 优化后:直接字符串拼接

import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer";

export function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div${_ssrRenderAttrs(_attrs)}><span>你好</span><span>${_ssrInterpolate(_ctx.message)}</span>...</div>`);
}

6. Vapor Mode(实验性编译策略)

Vue 的传统渲染路径是“模板 -> VNode -> Diff/Patch”。Vapor Mode 是另一种编译策略:在编译阶段把模板转换为更接近直接 DOM 操作的指令和响应式更新逻辑,减少 VNode 创建、树遍历和 Diff 的开销。它仍然依赖响应式系统,只是把更新目标尽量收敛到具体的 DOM 节点。

传统模式:模板 -> VNode/Block Tree -> patch -> DOM
Vapor:    模板 -> DOM 创建指令 + 精确更新 effect -> DOM

下面是表达思路的伪代码,具体生成代码和配置会随 Vue 版本变化:

// template: <button>{{ count }}</button>
const button = document.createElement('button');
const text = document.createTextNode('');
button.append(text);

effect(() => {
  text.data = String(count.value);
});

它更适合高频局部更新、超大列表和实时图表等场景,但不能把“跳过 VDOM”理解成所有页面都会更快:

  • Vapor Mode 在资料对应时期仍是实验性能力,通常需要特定编译器/运行时支持,不能默认当作稳定生产开关。
  • 动态组件、复杂插槽、第三方插件、SSR/水合、调试工具和生态兼容性都要单独验证。
  • PDF 中“万级列表约减少 40% 时间、内存接近减半”属于特定演示数据,不能当作普遍基准;上线前应使用真实业务数据测量首屏、更新耗时、内存和交互延迟。
  • 迁移时应保留回退路径,先在独立组件或低风险页面灰度,并观察错误率和可维护性。

来源:25年前端年度复盘.pdf 第 2 页。Vapor Mode 的具体 API、编译器和兼容范围应以采用版本的官方文档与基准测试为准。

二、源码体积优化

1. Tree Shaking

// Vue2: 无论使用什么功能,全部打包
import Vue from 'vue';
// 完整构建会包含响应式、编译器、运行时和组件系统;具体体积取决于版本与构建配置

// Vue3: 按需引入,未使用功能不打包
import { ref, computed, watch, nextTick } from 'vue';
// Vue3 可以按模块 Tree Shaking;最终体积应以生产构建产物为准

模块化架构:

Vue3 模块化结构
├── @vue/reactivity     # 响应式系统(可独立使用)
├── @vue/runtime-core   # 运行时核心
├── @vue/runtime-dom    # DOM 运行时
├── @vue/compiler-core  # 编译器核心
├── @vue/compiler-dom   # DOM 编译器
└── vue                 # 完整版
// 只使用响应式能力
import { reactive, effect } from '@vue/reactivity';

const state = reactive({ count: 0 });
effect(() => {
  console.log(state.count);
});

2. 运行时体积对比

场景 需要比较的指标 说明
运行时核心 生产压缩后体积 受版本、构建目标和压缩器影响
加入 compiler 增量体积 运行时编译与预编译模板的选择不同
按需引入 API 增量体积 只有经过 Tree Shaking 且无副作用时才会减少
组件/过渡能力 增量体积与运行时成本 结合实际使用路径测量,不用固定 KB 估算

三、响应式系统优化

1. Proxy vs Object.defineProperty

Vue2 的 defineProperty:

// 问题1: 初始化时需要深度递归遍历
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) return;

  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
    observe(obj[key]);  // 递归监听嵌套对象
  });
}

// 问题2: 无法监听新增/删除属性
const obj = { foo: 'foo' };
observe(obj);
obj.bar = 'bar';  // 无法监听!
delete obj.foo;   // 无法监听!

// 问题3: 数组需要特殊处理
const arr = [1, 2, 3];
observe(arr);
arr.push(4);  // 无法监听!需要重写数组方法

Vue3 的 Proxy:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      // 懒递归:访问时才建立响应式
      track(target, key);
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      const hadKey = hasOwn(target, key);
      const oldValue = target[key];
      const res = Reflect.set(target, key, value, receiver);

      // 自动处理新增属性
      if (!hadKey) {
        trigger(target, key, 'add');
      } else if (hasChanged(value, oldValue)) {
        trigger(target, key, 'set');
      }
      return res;
    },
    deleteProperty(target, key) {
      const hadKey = hasOwn(target, key);
      const res = Reflect.deleteProperty(target, key);
      if (hadKey) {
        trigger(target, key, 'delete');  // 自动处理删除
      }
      return res;
    }
  });
}

// 使用
const state = reactive({ foo: 'foo' });
state.bar = 'bar';    // ✅ 自动监听
state.nested = { a: 1 };
state.nested.a = 2;   // ✅ 自动监听嵌套属性

2. 性能对比

场景 Vue2 Vue3
初始化 深度递归,慢 懒加载,快
新增属性 无法监听(需 Vue.set) 自动监听
删除属性 无法监听(需 Vue.delete) 自动监听
数组索引 无法监听 自动监听
数组长度 无法监听 自动监听
Map/Set 不支持 支持

深入理解

1. 编译时优化 vs 运行时优化

┌─────────────────────────────────────────────────────┐
│                Vue3 优化策略                         │
├─────────────────────────────────────────────────────┤
│  编译时优化(Build Time)                             │
│  • 静态标记 - 减少运行时 Diff 比较                     │
│  • 静态提升 - 减少运行时对象创建                       │
│  • 事件缓存 - 减少不必要的更新追踪                     │
│  • Block Tree - 打平树结构,减少遍历深度               │
├─────────────────────────────────────────────────────┤
│  运行时优化(Runtime)                                │
│  • Proxy - 更快的响应式追踪                           │
│  • 优化过的组件实例创建                               │
│  • 更高效的调度算法                                   │
├─────────────────────────────────────────────────────┤
│  打包优化(Bundle)                                   │
│  • Tree Shaking - 按需打包                           │
│  • 更细粒度的模块拆分                                 │
└─────────────────────────────────────────────────────┘

2. PatchFlag 的位运算设计

// 位运算设计,可以组合多个标记
const patchFlag = PatchFlags.TEXT | PatchFlags.CLASS;
// 0001 | 0010 = 0011 = 3

// 检查时高效
if (patchFlag & PatchFlags.TEXT) {
  // 检查第1位是否为1
}
if (patchFlag & PatchFlags.CLASS) {
  // 检查第2位是否为1
}

3. 如何验证性能数据

不要把某个版本、机器或演示页面的百分比当作通用结论。对 Vue2/Vue3 或不同编译策略做对照时,应固定浏览器、设备、数据量、构建模式和交互脚本,分别记录首屏时间、更新耗时、长任务、内存峰值、包体和错误率;重复多次并报告测试条件,才能解释“提升”是否来自真实方案。

最佳实践

1. 利用编译优化编写高效模板

<!-- ✅ 好:静态内容多,动态内容少 -->
<template>
  <div class="article">
    <h1>{{ title }}</h1>
    <div class="content">
      <p>段落1</p>
      <p>段落2</p>
      <p>段落3</p>
    </div>
    <p class="author">作者:{{ author }}</p>
  </div>
</template>

<!-- ❌ 差:过度使用动态绑定 -->
<template>
  <div :class="articleClass">
    <h1 :class="titleClass">{{ title }}</h1>
    <div :class="contentClass">
      <p :class="pClass">段落1</p>
      <p :class="pClass">段落2</p>
    </div>
  </div>
</template>

2. 合理使用 v-once 和 v-memo

<template>
  <!-- 只渲染一次,后续跳过更新 -->
  <div v-once>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>

  <!-- 只有依赖变化时才更新 -->
  <div v-memo="[valueA, valueB]">
    <!-- 只有 valueA 或 valueB 变化时才重新渲染 -->
    <p>{{ valueA }} - {{ valueB }}</p>
    <p>{{ valueC }}</p>  <!-- valueC 变化不会触发更新 -->
  </div>
</template>

3. 选择正确的响应式 API

import { ref, reactive, shallowRef, shallowReactive, readonly } from 'vue';

// 基础类型用 ref
const count = ref(0);

// 对象类型用 reactive
const state = reactive({ count: 0, list: [] });

// 大型数据优化 - 浅层响应式
const bigList = shallowRef([/* 10000 条数据 */]);
// 只有 bigList.value = newList 会触发更新
// bigList.value[0].name = 'xxx' 不会触发

// 常量数据 - readonly
const config = readonly({ apiUrl: 'https://api.example.com' });

面试要点

  1. 能够说出编译阶段的优化手段知识点:编译阶段优化

    • 静态标记(PatchFlag)、静态提升、事件缓存、Block Tree
  2. 理解 PatchFlag 的作用知识点:Diff 算法优化 - PatchFlag

    • 标记动态节点类型,Diff 时只对比标记的属性
  3. 理解静态提升的原理知识点:静态提升

    • 静态节点提升到 render 函数外,只创建一次
  4. 掌握 Proxy 相比 defineProperty 的优势知识点:Proxy vs Object.defineProperty

    • 懒加载、自动监听新增/删除、支持 Map/Set
  5. 了解 Tree Shaking 的作用知识点:Tree Shaking

    • 按需打包,减小最终产物体积
  6. 能够说出 Block Tree 的作用知识点:Block Tree - 树结构打平

    • 打平树结构,只遍历动态节点
  7. 如何看待 Vue Vapor Mode?知识点:Vapor Mode(实验性编译策略)

    • 编译期生成更接近直接 DOM 更新的代码,减少 VNode/Diff 开销;当前应以实验性能力对待,结合真实场景验证收益和兼容性。

核心结论

  • Vue3 从编译时、运行时、打包时三个维度进行了全面优化
  • 编译时优化是 Vue3 性能提升的核心
  • PatchFlag 让 Diff 算法更加高效
  • Proxy 让响应式系统更加完善
  • Tree Shaking 显著减小了打包体积