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>`);
}
二、源码体积优化
1. Tree Shaking
// Vue2: 无论使用什么功能,全部打包
import Vue from 'vue';
// 打包结果包含:响应式、编译器、运行时、组件系统... 约 23KB
// Vue3: 按需引入,未使用功能不打包
import { ref, computed, watch, nextTick } from 'vue';
// 最小可至 10KB,使用多少打包多少
模块化架构:
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. 运行时体积对比
| 场景 | Vue2 | Vue3 |
|---|---|---|
| 运行时核心 | ~23KB | ~10KB |
| + compiler | + ~17KB | + ~22KB |
| 使用 computed | 已包含 | + ~0.5KB |
| 使用 watch | 已包含 | + ~0.5KB |
| 使用 transition | 已包含 | + ~1.5KB |
三、响应式系统优化
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. 实际性能数据
Vue3 官方基准测试
┌─────────────────────────┬──────────┬──────────┬──────────┐
│ 测试项 │ Vue2 │ Vue3 │ 提升 │
├─────────────────────────┼──────────┼──────────┼──────────┤
│ 创建组件(1000行) │ 100% │ 133% │ +33% │
│ 挂载(大量静态内容) │ 100% │ 243% │ +143% │
│ 更新(少量动态内容) │ 100% │ 237% │ +137% │
│ 内存占用 │ 100% │ 54% │ -46% │
└─────────────────────────┴──────────┴──────────┴──────────┘
最佳实践
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' });
面试要点
-
能够说出编译阶段的优化手段
- 静态标记(PatchFlag)、静态提升、事件缓存、Block Tree
-
理解 PatchFlag 的作用
- 标记动态节点类型,Diff 时只对比标记的属性
-
理解静态提升的原理
- 静态节点提升到 render 函数外,只创建一次
-
掌握 Proxy 相比 defineProperty 的优势
- 懒加载、自动监听新增/删除、支持 Map/Set
-
了解 Tree Shaking 的作用
- 按需打包,减小最终产物体积
-
能够说出 Block Tree 的作用
- 打平树结构,只遍历动态节点
核心结论:
- Vue3 从编译时、运行时、打包时三个维度进行了全面优化
- 编译时优化是 Vue3 性能提升的核心
- PatchFlag 让 Diff 算法更加高效
- Proxy 让响应式系统更加完善
- Tree Shaking 显著减小了打包体积