Vue 3.0 中 Tree Shaking 特性是什么?举例说明一下?
问题解析
Tree Shaking 是 Vue3 重要的打包优化特性。面试考察这个问题,是想要了解候选人对现代前端工程化的理解,以及能否从源码层面理解 Vue3 的模块化设计。
核心概念
什么是 Tree Shaking
┌─────────────────────────────────────────────────────┐
│ Tree Shaking 原理 │
├─────────────────────────────────────────────────────┤
│ │
│ 源代码 打包过程 输出 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │ import │ ───▶ │ 分析依赖 │ ───▶ │ 使用代码 ││
│ │ A, B │ │ 关系图 │ │ (A) ││
│ │ C, D │ │ │ │ ││
│ └─────────┘ │ B, C, D │ │ B,C,D ││
│ 使用 A │ 未被使用 │ ───▶ │ 被删除 ││
│ └─────────┘ └─────────┘│
│ │
│ 专业术语:Dead Code Elimination(消除无用代码) │
└─────────────────────────────────────────────────────┘
Tree Shaking 的工作原理
// 1. ES Module 的静态结构
// 编译时就能确定模块的依赖关系
import { foo, bar } from './module.js';
// 2. 基于 import/export 的静态分析
// • import { foo } - 只用 foo,bar 可以被摇掉
// • export const bar - 没有被导入,可以被摇掉
// 3. 副作用标记(sideEffects)
// package.json 中标记哪些文件有副作用,不能删除
{
"sideEffects": [
"*.css",
"*.global.js"
]
}
详细解答
一、Vue2 vs Vue3 的对比
Vue2 - 全量引入
// 方式1: 完整引入
import Vue from 'vue';
// 使用部分功能
Vue.nextTick(() => {});
new Vue({
data: { count: 0 },
computed: {
double() { return this.count * 2; }
}
});
// 打包结果:包含 Vue 完整运行时(约 23KB gzipped)
// 即使只使用了 nextTick 和基础功能,也打包了所有代码
Vue2 打包内容
┌─────────────────────────────────────────┐
│ Vue 构造函数 │
│ • 响应式系统 (observer) │
│ • 模板编译器 (compiler) - 可能不需要 │
│ • 虚拟 DOM (vdom) │
│ • 组件系统 (component) │
│ • 全局 API (set, delete, nextTick等) │
│ • 内置组件 (transition, keep-alive等) │
│ • 指令系统 (directives) │
│ • 过滤器 (filters) │
│ • ... 其他所有功能 │
└─────────────────────────────────────────┘
↓ 全部打包,无法选择
Vue3 - 按需引入
// 按需导入需要的功能
import { createApp, ref, computed, nextTick, watch } from 'vue';
const app = createApp({
setup() {
const count = ref(0);
const double = computed(() => count.value * 2);
nextTick(() => {
console.log('DOM 更新完成');
});
watch(count, (newVal) => {
console.log('count changed:', newVal);
});
return { count, double };
}
});
// 打包结果:只包含使用到的功能(最小可至 10KB gzipped)
Vue3 打包内容
┌─────────────────────────────────────────┐
│ 使用到的功能 │
│ • createApp │
│ • ref + effect │
│ • computed │
│ • nextTick │
│ • watch │
│ • 虚拟 DOM (runtime) │
├─────────────────────────────────────────┤
│ 未使用,被 Tree Shaking 移除 │
│ • transition (未使用) │
│ • keep-alive (未使用) │
│ • suspense (未使用) │
│ • teleport (未使用) │
│ • 其他未使用的 API │
└─────────────────────────────────────────┘
二、Vue3 的模块化架构
Vue3 Monorepo 结构
├─ packages/
│ ├─ @vue/reactivity # 响应式系统(可独立使用)
│ │ ├─ ref
│ │ ├─ reactive
│ │ ├─ computed
│ │ ├─ watch
│ │ └─ effect
│ │
│ ├─ @vue/runtime-core # 运行时核心
│ │ ├─ component
│ │ ├─ vnode
│ │ ├─ scheduler
│ │ └─ renderer
│ │
│ ├─ @vue/runtime-dom # DOM 运行时
│ │ ├─ patch
│ │ ├─ nodeOps
│ │ └─ modules/
│ │ ├─ class
│ │ ├─ style
│ │ ├─ events
│ │ └─ domProps
│ │
│ ├─ @vue/compiler-core # 编译器核心
│ ├─ @vue/compiler-dom # DOM 编译器
│ │
│ ├─ @vue/shared # 共享工具
│ │
│ └─ vue # 完整版入口
│ ├─ runtime-only # 仅运行时
│ └─ full # 完整版(含编译器)
三、具体示例演示
示例1:基础响应式
// 代码
import { ref, reactive } from 'vue';
const count = ref(0);
const state = reactive({ name: 'Tom' });
// 打包结果分析
// ✅ 包含:ref, reactive, effect (依赖)
// ❌ 不包含:computed, watch, readonly, shallowRef 等
示例2:增加 computed
// 代码
import { ref, computed } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
// 打包结果分析
// ✅ 包含:ref, computed + 相关依赖
// 体积增加约 0.5KB
示例3:增加 watch
// 代码
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
watch(count, (newVal) => {
console.log(newVal);
});
watchEffect(() => {
console.log(count.value);
});
// 打包结果分析
// ✅ 包含:ref, watch, watchEffect + 相关依赖
// 体积增加约 0.5KB
示例4:使用内置组件
<template>
<div>
<!-- 使用 Transition -->
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
<!-- 未使用 KeepAlive -->
<!-- 未使用 Teleport -->
<!-- 未使用 Suspense -->
</div>
</template>
<script setup>
import { ref } from 'vue';
const show = ref(true);
</script>
<!-- 打包结果分析 -->
<!-- ✅ 包含:ref, Transition 相关代码 -->
<!-- ❌ 不包含:KeepAlive, Teleport, Suspense -->
四、实际打包体积对比
Vue3 功能与打包体积对应表
┌─────────────────────┬───────────┬─────────────┐
│ 功能组合 │ 体积(gz) │ 相对基准 │
├─────────────────────┼───────────┼─────────────┤
│ ref 最小使用 │ ~10KB │ 100% │
├─────────────────────┼───────────┼─────────────┤
│ + computed │ ~10.5KB │ 105% │
├─────────────────────┼───────────┼─────────────┤
│ + watch │ ~11KB │ 110% │
├─────────────────────┼───────────┼─────────────┤
│ + transition │ ~12.5KB │ 125% │
├─────────────────────┼───────────┼─────────────┤
│ + keep-alive │ ~13KB │ 130% │
├─────────────────────┼───────────┼─────────────┤
│ + teleport │ ~13.5KB │ 135% │
├─────────────────────┼───────────┼─────────────┤
│ 完整运行时 │ ~22KB │ 220% │
├─────────────────────┼───────────┼─────────────┤
│ 完整版(含编译器) │ ~35KB │ 350% │
└─────────────────────┴───────────┴─────────────┘
五、Vue3 源码中的 Tree Shaking 设计
// Vue3 源码中的导出设计
// reactivity/index.ts
export {
ref,
shallowRef,
isRef,
toRef,
toRefs,
unref,
proxyRefs,
customRef,
triggerRef,
Ref,
ToRef,
ToRefs,
UnwrapRef,
ShallowUnwrapRef,
// ... 每个都是独立导出
} from './ref'
export {
reactive,
readonly,
shallowReactive,
shallowReadonly,
// ...
} from './reactive'
export {
computed,
ComputedRef,
WritableComputedRef,
ComputedGetter,
ComputedSetter,
WritableComputedOptions
} from './computed'
export {
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
WatchEffect,
WatchOptions,
WatchSource,
WatchCallback,
WatchStopHandle
} from './apiWatch'
// 每个功能独立导出,便于 Tree Shaking
深入理解
1. Tree Shaking 的必要条件
// ✅ 1. 使用 ES Module(静态导入导出)
import { foo } from 'module'; // 可以 Tree Shaking
// ❌ 2. CommonJS 无法 Tree Shaking
const { foo } = require('module'); // 无法分析
// ✅ 3. 导出必须是静态的
export { foo, bar }; // 可以 Tree Shaking
// ❌ 4. 动态导出无法 Tree Shaking
module.exports[dynamicKey] = value;
2. 副作用的影响
// 文件: utils.js
export const foo = () => 'foo';
export const bar = () => 'bar';
// 副作用代码:在模块加载时执行
console.log('模块被加载了'); // 即使没有导入,这段代码也会执行
// package.json
{
"sideEffects": false // 标记为无副作用,可以安全 Tree Shaking
}
// 或者指定有副作用的文件
{
"sideEffects": [
"*.css",
"*.less",
"*.global.js",
"./src/polyfill.js"
]
}
3. Webpack 的 Tree Shaking 配置
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
// 启用 Tree Shaking
usedExports: true,
// 代码分割,进一步减少主包体积
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
},
// 模块合并优化
concatenateModules: true,
// 压缩时删除无用代码
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
unused: true, // 删除未使用的变量
dead_code: true // 删除不会执行的代码
}
}
})
]
}
};
4. Rollup 的 Tree Shaking(Vue3 使用)
// rollup.config.js
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm' // ES Module 格式最优
},
// Tree Shaking 相关选项
treeshake: {
// 假设没有副作用(除非标记)
moduleSideEffects: false,
// 尝试删除未使用的代码
propertyReadSideEffects: false,
// 保留 debugger 语句
tryCatchDeoptimization: false
}
};
最佳实践
1. 按需导入 Vue3 API
// ❌ 不要这样写
import Vue from 'vue';
// ✅ 按需导入
import { ref, computed, watch, nextTick } from 'vue';
// ❌ 避免导入未使用的 API
import {
ref,
reactive,
computed,
watch,
watchEffect, // 没用到
readonly, // 没用到
shallowRef // 没用到
} from 'vue';
// ✅ 只导入需要的
import { ref, reactive, computed, watch } from 'vue';
2. 第三方库的 Tree Shaking
// ❌ 全量导入 lodash(约 70KB)
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ 按需导入(约 5KB)
import debounce from 'lodash/debounce';
// ✅ 或使用 lodash-es
import { debounce, throttle } from 'lodash-es';
// ❌ 全量导入 Element Plus
import ElementPlus from 'element-plus';
// ✅ 按需导入组件
import { ElButton, ElInput } from 'element-plus';
3. 编写可 Tree Shaking 的代码
// ✅ 使用具名导出
export const foo = () => {};
export const bar = () => {};
// ✅ 纯函数(无副作用)
export const add = (a, b) => a + b;
// ❌ 避免在模块顶层执行副作用
const cache = {}; // ✅ 可以,初始化是允许的
console.log('loaded'); // ❌ 副作用,影响 Tree Shaking
document.addEventListener('click', handler); // ❌ 副作用
// ✅ 将副作用包装在函数中
export function init() {
document.addEventListener('click', handler);
}
4. 检查和优化打包结果
# 使用 webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# vue.config.js
module.exports = {
chainWebpack: config => {
config
.plugin('webpack-bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin);
}
};
# 使用 rollup-plugin-visualizer
npm install --save-dev rollup-plugin-visualizer
# vite 内置支持
npm run build -- --analyze
面试要点
-
能够解释 Tree Shaking 的原理
- 基于 ES Module 的静态结构
- 编译时确定依赖关系
- 消除未使用的代码(Dead Code Elimination)
-
对比 Vue2 和 Vue3 的打包差异
- Vue2:单例模式,无法分析使用哪些功能
- Vue3:模块化设计,按需导入
-
理解 Vue3 的模块化架构
- runtime-core, runtime-dom, reactivity 等独立包
- 可以单独使用 @vue/reactivity
-
知道 Tree Shaking 的必要条件
- ES Module
- 无副作用(或正确标记)
- 静态导入导出
-
能够说出实际优化效果
- 最小可至 10KB(仅 ref)
- 对比 Vue2 的 23KB 有显著减少
- 使用越多,体积越大,但始终小于 Vue2
核心结论:
- Tree Shaking 是现代前端工程化的重要优化手段
- Vue3 的模块化设计完美支持 Tree Shaking
- 使用时应按需导入,避免引入未使用的功能
- 可以显著减少最终打包体积,提升加载速度