返回首页

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' });

面试要点

  1. 能够说出编译阶段的优化手段

    • 静态标记(PatchFlag)、静态提升、事件缓存、Block Tree
  2. 理解 PatchFlag 的作用

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

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

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

    • 按需打包,减小最终产物体积
  6. 能够说出 Block Tree 的作用

    • 打平树结构,只遍历动态节点

核心结论

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