什么是Tree-shaking?
通俗地说把你的代码比作一颗“树”,源代码是活的树叶,而那些虽然写了但没被用到的代码就是“死树叶”。通过摇晃这棵树,让死树叶掉落,最终只把有用的代码打包进产物
Tree-shaking 必须依赖于 ES6 的模块系统(import/export)。
在 CommonJS(require())时代,代码是动态加载的。由于 require 可以写在 if 语句里,打包工具在静态分析时根本无法确定哪些代码会被用到。
而 ESM 具有以下 “静态特性”:
- 只能顶层出现:import 不能写在函数或判断条件里。
- 模块名必须是字符串常量:不能动态计算路径。
- 导出的绑定是只读的。
这些限制让 Webpack 或 Rollup 能够在不运行代码的情况下,通过 AST(抽象语法树) 静态地推导出哪些变量和函数被引用了
Vue3相比于Vue2,做了Tree-shaking优化,在没有该优化之前,Vue2是怎么打包的呢?
Vue2打包流程:
在 Vue 2 的时代,打包工具(主要是 Webpack)的流程如下:
- 引用声明:你在 main.js 中写下 import Vue from 'vue'。
- 依赖追踪:Webpack 顺着这条线找到 Vue 的源码入口。
- 全量引入:Vue 2 的核心代码是挂载在一个巨大的类/对象(即 Vue 构造函数)上的。Vue.component、Vue.directive、Vue.nextTick 等所有功能都在这个对象的原型链或静态属性上。
//相当于引入的是一个巨大的构造函数,所有 API 都在 Vue 对象上
import Vue from 'vue';
- 无法“摇晃”:对于 Webpack 来说,由于你引用了 Vue 这个对象,而这个对象内部引用了所有的内置功能,Webpack 不敢确定你以后会不会通过 this.$nextTick 或 v-model 调用它们。结果:为了保证运行不出错,Webpack 只能把整个 Vue 运行时的代码全量打包进你的 vendor.js
为什么?
Vue 2 的设计初期是为了兼容旧环境,采用了更传统的“单体类”模式。
API 挂载方式:Vue 2 大量依赖 this 上下文。比如 this.$set、this.$emit,这些功能必须预先挂载到原型链上才能通过 this 访问
Vue2插件(比如Vue router、Vuex)
基本也是全量打包,很难做到摇树优化
在 Vue 2 时代,Vue Router 和 Vuex 都是通过 ES6 Class 导出
//引入的是一个完整的类
import VueRouter from 'vue-router'
- 方法挂载在原型链上:路由的所有功能(如 push、replace、addRoutes、matcher 逻辑等)都定义在 VueRouter.prototype 上。
- 工具无法判断调用:Tree-shaking 工具(如 Webpack 的 Terser)主要依赖静态分析。由于 JavaScript 是动态语言,工具无法确定你是否在代码的某个角落通过 router['push']() 这种方式动态调用了方法
- 全局污染:Vue.use 会执行插件内部的 install 方法,该方法通常会直接修改全局的 Vue 对象(例如执行 Vue.prototype.$router = ...)。
- 不可预测性:打包工具看到这种“修改全局变量”的操作,会将其标记为“有副作用的”
Vue3的Tree-shaking优化
Vue 3 采用了 解耦设计。几乎所有的 API(如 computed, watch, ref)都是通过 named export 导出的
运行打包命令时,经过以下过程:
- 入口扫描:从 main.js 开始。
- 依赖解析:main.js 引入了 App.vue,App.vue 又引入了 router 和各个 pages。
- 构建依赖图:打包工具会顺着这些 import 语句,把所有用到的 .vue、.js、.ts 文件像拉家谱一样连成一张巨大的网。
Vue 3 官方库本身就是按需导出
- 如果你的项目里所有页面都只用了 ref,没用 reactive 或 watch。
- 打包工具在扫描 Vue 源码包时,发现 reactive 和 watch 的导出没有被任何页面引用。
- 结果:最终生成的 vendor.js(第三方库包)里,就不包含 Vue 源码中关于 reactive 的那部分代码。
Vue3插件(Vue router、pinia/vuex)
Vue 3 的核心思想是从 “面向对象” 转向 “函数式
Vue 3 不再需要 new VueRouter(),而是提供一系列具名导出的函数。
// Vue 3 路由按需引入
import { createRouter, createWebHistory } from 'vue-router'
如果只用了 createWebHistory,那么关于 Hash 模式的代码就完全不会被包含在最终的包里。
Vue 3 官方推荐的 Pinia 以及新版 Vuex 4,底层都大量使用了 Vue 3 的组合式 API。
- 原理:每一个 Store 都是一个独立的函数。如果你定义了 10 个 Store 但只用了 2 个,另外 8 个在生产环境打包时会被直接“摇”掉
不同构建工具如何实现Tree-shaking
Tree-shaking 的本质是“静态路径追踪 + 标记 + 物理删除”
Tree-shaking并不是vue3特有的概念,vue3把功能全部解耦成了一个个独立的函数导出,使得打包时可以进行tree-shaking优化
Tree-shaking 的具体实现逻辑在 Rollup(Vite 的生产环境内核)或 Webpack 里面
执行流程:
- 标记(Marking):构建工具扫描整个项目的依赖树,给每个函数或变量打上“引用”或“未引用”的标签。
- 副作用判定(Side Effects):工具会分析一段代码如果被删掉,会不会影响全局(比如有没有修改 window)。
- 压缩剔除(DCE - Dead Code Elimination):Webpack:通过 Terser 等插件,把标记为 unused 的代码物理抹除。Vite (Rollup):在生成 Bundle 时,只从源码中“挑出”有用的部分进行组合。