返回首页

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

面试要点

  1. 能够解释 Tree Shaking 的原理

    • 基于 ES Module 的静态结构
    • 编译时确定依赖关系
    • 消除未使用的代码(Dead Code Elimination)
  2. 对比 Vue2 和 Vue3 的打包差异

    • Vue2:单例模式,无法分析使用哪些功能
    • Vue3:模块化设计,按需导入
  3. 理解 Vue3 的模块化架构

    • runtime-core, runtime-dom, reactivity 等独立包
    • 可以单独使用 @vue/reactivity
  4. 知道 Tree Shaking 的必要条件

    • ES Module
    • 无副作用(或正确标记)
    • 静态导入导出
  5. 能够说出实际优化效果

    • 最小可至 10KB(仅 ref)
    • 对比 Vue2 的 23KB 有显著减少
    • 使用越多,体积越大,但始终小于 Vue2

核心结论

  • Tree Shaking 是现代前端工程化的重要优化手段
  • Vue3 的模块化设计完美支持 Tree Shaking
  • 使用时应按需导入,避免引入未使用的功能
  • 可以显著减少最终打包体积,提升加载速度