简单聊聊webpack摇树的原理
Webpack 的 Tree Shaking(摇树)是一项用于消除 JavaScript 上下文中未引用代码的优化手段,它能有效减小打包体积。
核心原理
Tree Shaking 的本质是 死代码消除,它依赖 ES6 模块(ESM)的静态语法结构。
-
静态分析:ESM 的
import/export语句必须位于模块顶层(注意:模块顶层不是模块文件顶部的意思,模块顶层可以认为是模块文件中最外层的代码区,不在任何函数、类或代码块内部),且模块路径必须是字符串常量。这样, Webpack 在编译阶段就能构建出完整的模块依赖图,无需运行代码即可分析出哪些导出值未被其他模块使用 。这时有同学就会问了,那么动态 import 怎么判断呢?
其实,还是那个关键点,是否可以被“静态分析”。
// ❌ 难以静态分析,无法使用摇树优化 const componentMap = { basic: () => import('./BasicComponent'), advanced: () => import('./AdvancedComponent') }; const getComponent = componentMap[userInput]; // 运行时才能确定 // ✅ 条件明确,可以被静态分析 if (import.meta.env.VITE_APP_MODE === 'basic') { const BasicComponent = await import('./BasicComponent'); } -
标记与清除:Webpack 的 Tree Shaking 过程大致分为两步。首先,在编译阶段,Webpack 会遍历所有模块,标记(Mark) 出未被使用的导出(通常会在注释中生成类似
unused harmony export的提示)。随后,在代码压缩阶段,Terser 等压缩工具会真正将标记过的"死代码"清除(Shake) 掉 。
这些配置你是否清楚?
要让 Tree Shaking 生效,需要同时满足以下条件:
-
使用 ES6 模块语法:必须使用
import和export语句。CommonJS 的require和module.exports是动态的,无法在编译时进行静态分析,因此不支持 Tree Shaking 。 -
启用生产模式或明确配置:在 Webpack 配置中,将
mode设置为'production'生产模式下会自动开启相关的优化功能。当然也可以在开发模式下手动配置optimization.usedExports和optimization.minimize。
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动开启优化
optimization: {
usedExports: true, // 启用使用导出分析
minimize: true // 启用代码压缩(清除死代码)
}
};
- **正确声明副作用 (
sideEffects)**:在项目的package.json中,通过sideEffects属性告知 Webpack 哪些文件是"纯净"的(无副作用),可以安全移除。这能防止具有副作用的文件(如全局样式表、polyfill)被误删 。
// package.json
{
"sideEffects": false, // 表示整个项目都没有副作用
// 或明确指定有副作用的文件
"sideEffects": [
"**/*.css",
"./src/polyfill.js"
]
}
有同学又会问了,摇树摇的不是 js 吗,样式表 css 怎么会被摇掉呢?
其实,这里指的是导入的但是没有明确导出的 css 样式表,导入导出是明确的 js 语句,css 是“副作用”,比如:
- 仅导入但未使用任何导出(如
import './style.css'),属于是无形的“使用”,可能被误删- 使用 CSS Modules(如
import styles from './Component.module.css'),被视为有被使用的对象(如styles.className),通常不会被误删
这些问题你遇到过吗?
开发过程中,以下情况仍可能导致 Tree Shaking 失效,看看你有没有遇到过:
-
Babel 配置不当:Babel 预设
@babel/preset-env可能会将 ESM 转换为 CommonJS。务必确保其modules选项设置为false,只有 ESM 可以摇树。
// .babelrc
{
"presets": [["@babel/preset-env", { "modules": false }]]
}
-
第三方库的模块版本:优先选择提供 ES6 模块版本的库(如使用
lodash-es而非lodash),并采用按需导入的方式 。
// 推荐:按需导入
import { debounce } from 'lodash-es';
// 不推荐:整体导入
import _ from 'lodash';
- 导出粒度太粗:尽量使用具名导出而非默认导出对象,有助于进行更精细的分析 。
// 推荐:细粒度导出
export function func1() {}
export function func2() {}
// 谨慎使用:粗粒度导出(不利于分析内部未使用属性)
export default { func1, func2 };