人人都会AI编程

22.1 Tree Shaking 与按需引入

更新时间:2026-07-11

打包产物的体积直接影响页面加载速度,而Tree Shaking按需引入是控制体积最直接的两个手段。它们的共同目标都是:只把真正用到的代码打进最终产物,其余的一律丢掉

Tree Shaking 是什么

Tree Shaking 的字面意思是“摇树”——想象一棵树的枯叶,用力一摇,枯叶落下,剩下健康的枝叶。在打包领域,它指的是 ES Module(ESM)静态导入/导出语法下,打包工具在构建时剔除没有被使用的代码

为什么必须是 ESM 语法?因为 importexport 是静态结构,打包工具可以在编译阶段就分析出哪些导出被引用了、哪些沉睡着。CommonJS 的 require() 是动态的,无法在构建时进行可靠的依赖分析。

一个简单的例子:

// utils.js
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
// main.js
import { add } from './utils.js'
console.log(add(1, 2))

最终打包产物中,subtract 函数会被自动剔除,因为它从未在任何地方被引入。这个剔除由打包工具(Vite 底层使用的 Rollup,或者 Webpack 的生产模式)自动完成,前提是模块使用 ESM 且没有被副作用影响。

在 Vue 项目中的 Tree Shaking

Vue 3 本身就是按 ESM 标准构建的,所有 API 都通过具名导出暴露:

// 只会把 ref、computed 打包进去,其他如 watch、reactive 等不会被包含
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)

你不需要额外配置,只要用的是 Vite(内部用 Rollup)或 Webpack 的生产模式,Tree Shaking 默认生效。但一个常见的坑是副作用(side effects)。有些模块除了导出函数,还会在顶层执行一段代码(比如注册一个全局变量),打包工具不敢随意移除这种文件,因为“副作用”可能是有意为之。

package.json 中声明 "sideEffects": false 可以告诉打包工具“本包的所有模块都是无副作用的,不引入就可以安全删除”。Vue 3 的包已经正确声明了该字段,社区优质库也大多如此。例如 Element Plus、Ant Design Vue 等也都做了类似处理。

按需引入:组件库体积瘦身的正道

用到组件库时,“Tree Shaking”不能解决所有问题。因为组件库的入口往往是全量注册:

// 错误:这样会拖入整个组件库
import ElementPlus from 'element-plus'
app.use(ElementPlus)

此时就算你只用了 ElButton,打包产物里也会包含所有组件的代码。正确的做法是“按需引入”——手动只引入你用到的组件和样式,或者使用支持自动按需引入的插件。

方式一:手动按需引入

import { ElButton, ElInput } from 'element-plus'
import 'element-plus/es/components/button/style/css'
import 'element-plus/es/components/input/style/css'

// 手动注册
app.component(ElButton.name, ElButton)
app.component(ElInput.name, ElInput)

这种方式完全由你控制,但比较繁琐。好在现代组件库都提供了更优雅的自动方案。

方式二:通过插件自动按需引入
Element Plus 提供了 unplugin-element-plus

npm install -D unplugin-element-plus

vite.config.js 中引入插件:

import ElementPlus from 'unplugin-element-plus/vite'

export default {
  plugins: [
    ElementPlus(), // 会自动转换按需引入
  ]
}

Ant Design Vue 也有类似的 unplugin-vue-components,能自动导入组件并处理样式。配置后,你可以直接写组件标签而不用手动注册。

方式三:使用专用按需引入的构建产物
一些库(如 Vant、Element Plus)会在 es 目录下输出每个组件独立的 ESM 文件,你可以配合 babel-plugin-importunplugin-auto-import 来在编译阶段转换导入路径。不过更推荐用上述插件的现代方案。

工具库的按需引入

工具库同样可能成为体积重灾区。一个典型反例:

import _ from 'lodash'
console.log(_.cloneDeep(obj))

此时整个 Lodash 被打包(超过 500KB)。正确姿势是只引入用到的函数:

import cloneDeep from 'lodash-es/cloneDeep'
console.log(cloneDeep(obj))

或者使用专门适配了 Tree Shaking 的工具库,如 lodash-es(同样支持 ESM)。在用 date-fnsrxjs 时,也是同样的原则:使用支持 ESM 具名导出的方式引入,打包工具会自动剔除未使用的部分

无用代码剔除与副作用处理

需要留意一种暗坑:你引入了一个看似“没用”的变量,但实际上它的副作用在你的代码中产生了效果。例如:

// configure.js —— 有副作用
console.log('全局配置已加载')
export const config = { theme: 'dark' }
// main.js
import { config } from './configure.js'

这里 configure.js 中的 console.log 是副作用,打包工具不能移除该模块,尽管你可能只用了 config。如果确实不需要副作用,可以在 package.json 中标记 "sideEffects": false,或在模块设计时避免无意义的全局副作用。

如果你的项目里有某些文件确实需要副作用(比如全局 CSS、初始化脚本),可以在 package.jsonsideEffects 数组中显式声明,避免被 Tree Shaking 误删:

"sideEffects": [
  "*.css",
  "*.scss",
  "./src/init.js"
]

这样打包工具只会安全地移除其它无副作用且未被引用的模块。

总结实践清单

  • 使用 ESM 语法:所有模块用 import / export 而非 CommonJS。
  • 选择支持 Tree Shaking 的库:使用 Vue 3、Pinia、lodash-es、date-fns 等天然支持 ESM 的库。
  • 组件库按需引入:借助 unplugin-vue-components 或库自带的按需插件,只加载用到的组件。
  • 回避全量导入:杜绝 import x from 'xxx' 后直接注册所有的操作(除非你确信库极小)。
  • 标记副作用文件:在 package.json 中声明 sideEffects,避免误删真正需要执行的副作用代码。
  • 定期分析体积:使用 rollup-plugin-visualizer 或 webpack-bundle-analyzer 检查产物构成,发现“巨型”模块并及时优化。

遵循这些原则后,一个中大型 Vue 项目的首屏 JS 体积可以轻松控制在 200KB 以内(gzip 后),而一个轻量的活动页甚至不到 50KB。Tree Shaking 与按需引入看似是“打包工具的事”,但你的代码组织和引入习惯才是决定它们是否生效的关键。