玩转 Webpack 的 TreeShaking 与 sideEffects 特性|原理篇
30 秒速记
- 核心判断:构建工具从入口解析依赖图,经转换和优化生成可在目标运行时加载的模块与资源
- 原理主线:围绕 「Tree Shaking」、「sideEffects」、「写在最后」 建立输入、状态变化与输出之间的因果关系
- 文章范围:讲解了 Webpack 的 Tree Shaking 和 sideEffects 特性,分析其原理、配置方法及在前端项目中的实际应用,帮助开发者理解如何通过移除未引用代码和合理配置 sideEffects 字段,实现更小的打包体积和更高效的
- 边界与代价:静态分析受模块语法、动态依赖和副作用声明限制;拆包过细也会增加请求和运行时成本
- 工程落地:优化前先测构建阶段、产物组成和缓存命中,再针对解析、转换、压缩或运行时逐层处理
Tree Shaking 会标记并移除未使用的导出,而 sideEffects 用来告诉 Webpack 哪些模块不能因未被引用就整段删除。 它依赖静态的 ES Modules,通常由 usedExports 配合代码压缩完成,生产模式会自动启用相关优化。若 Babel 把模块转成 CommonJS,静态分析就可能失效,因此一般将 @babel/preset-env 的 modules 设为 false。对含全局样式或初始化逻辑的模块,不能误标为无副作用。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 旧文中的 webpack 4、JSONP 更新清单或 react-hot-loader 代码用于解释历史链路,不应直接复制到新项目。当前 webpack-dev-server 4+ 默认启用 HMR,严格 ESM 可使用 import.meta.webpackHot;生产环境不得携带 HMR runtime。以 webpack HMR 官方指南 和项目锁定版本为准。
# Tree Shaking
Tree Shaking 翻译过来的意思就是“摇树”。伴随着摇树的动作,树上的枯树枝和树叶就会掉落下来。
我们这里要介绍的 Tree-shaking 也是同样的道理,不过通过 Tree-shaking “摇掉”的是代码中那些没有用到的部分,这部分没有用的代码更专业的说法应该叫作未引用代码(dead-code)。
Tree-shaking 最早是 Rollup 工具中推出的一个特性,Webpack 从 2.0 过后开始支持这个特性。
我们使用 Webpack 生产模式打包过程中,就会自动开启这个功能,以此来检测我们代码中的未引用代码,然后自动移除它们,从而达到对打包结果的优化。
我们可以先来体验一下这个功能的效果,这里我的源代码非常简单,只有两个文件。
└─ 09-tree-shaking
├── src
│ ├── components.js
│ └── main.js
├── package.json
└── webpack.config.js
其中 components.js 中导出了一些函数,这些函数各自模拟了一个组件,具体代码如下:
// ./src/components.js
export const Button = () => {
return document.createElement('button')
console.log('dead-code')
}
export const Link = () => {
return document.createElement('a')
}
export const Heading = level => {
return document.createElement('h' + level)
}
其中 Button 组件函数中,在 return 过后还有一个 console.log() 语句,很明显这句代码永远都不会被执行,所以这个 console.log() 就属于未引用代码。
此外,在 main.js 文件中只是导入了 compnents.js,具体代码如下:
// ./src/main.js
import { Button } from './components'
document.body.appendChild(Button())
注意这里导入 components 模块时,我们只提取了模块中的 Button 成员,那这就导致 components 模块中很多地方都不会被用到,那这些地方就是冗余的,具体冗余部分如下:
// ./src/components.js
export const Button = () => {
return document.createElement('button')
// 未引用代码
console.log('dead-code')
}
// 未引用代码
export const Link = () => {
return document.createElement('a')
}
// 未引用代码
export const Heading = level => {
return document.createElement('h' + level)
}
去除冗余代码是生产环境优化中一个很重要的工作,Webpack 的 Tree-shaking 功能就很好地实现了这一点。
我们打开命令行终端,这里我们尝试以 production 模式运行打包,具体命令如下:
$ npx webpack --mode=production
Webpack 的 Tree-shaking 特性在生产模式下会自动开启。打包完成以后我们打开输出的 bundle.js,具体结果如下:

通过搜索你会发现,components 模块中冗余的代码根本没有输出。这就是经过 Tree-shaking 处理过后的效果。
试想一下,如果我们在项目中引入 Lodash 这种工具库,大部分情况下我们只会使用其中的某几个工具函数,而其他没有用到的部分就都属于冗余代码。通过 Tree-shaking 就可以极大地减少最终打包后 bundle 的体积。
需要注意的是,Tree-shaking 并不是指 Webpack 中的某一个配置选项,而是一组功能搭配使用过后实现的效果,这组功能在生产模式下都会自动启用,所以使用生产模式打包就会有 Tree-shaking 的效果。
# 开启 Tree Shaking
由于目前官方文档中对于 Tree-shaking 的介绍有点混乱,所以我们这里再来介绍一下在其他模式下,如何一步一步手动开启 Tree-shaking。通过这个过程,还可以顺便了解 Tree-shaking 的工作过程和 Webpack 其他的一些优化功能。
这里还是使用上述的案例结构,我们再次运行 Webpack 打包,不过这一次我们不再使用 production 模式,而是使用 none,也就是不开启任何内置功能和插件,具体命令如下:
$ npx webpack --mode=none
打包完成过后,我们再次找到输出的 bundle.js 文件,具体结果如下:

我们这里注意一下 components 对应的这个模块,虽然外部没有使用这里的 Link 函数和 Heading 函数,但是仍然导出了它们,具体如下图所示:

显然这种导出是没有任何意义的。
明确目前打包结果的状态过后,我们打开 Webpack 的配置文件,在配置对象中添加一个 optimization 属性,这个属性用来集中配置 Webpack 内置优化功能,它的值也是一个对象。
在 optimization 对象中我们可以先开启一个 usedExports 选项,表示在输出结果中只导出外部使用了的成员,具体配置代码如下:
// ./webpack.config.js
/** @type {import('webpack').Configuration} */
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true
}
}
配置完成后,重新打包,然后我们再来看一下输出的 bundle.js,具体结果如下图:

此时你会发现 components 模块所对应的函数,就不再导出 Link 和 Heading 这两个函数了,那它们对应的代码就变成了未引用代码。而且如果你使用的是 VS Code,会发现 VS Code 将这两个函数名的颜色变淡了,这是为了表示它们未被引用。
对于这种未引用代码,如果我们开启压缩代码功能,就可以自动压缩掉这些没有用到的代码。
我们可以回到配置文件中,尝试在 optimization 配置中开启 minimize,具体配置如下:
// ./webpack.config.js
/** @type {import('webpack').Configuration} */
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 压缩输出结果
minimize: true
}
}
然后再次回到命令行重新运行打包,具体结果如下图所示:

仔细查看打包结果,你会发现,Link 和 Heading 这些未引用代码都被自动移除了。
这就是 Tree-shaking 的实现,整个过程用到了 Webpack 的两个优化功能:
- usedExports - 打包结果中只导出外部用到的成员;
- minimize - 压缩打包结果。
如果把我们的代码看成一棵大树,那你就可以这样理解:
- usedExports 的作用就是标记树上哪些是枯树枝、枯树叶;
- minimize 的作用就是负责把枯树枝、枯树叶摇下来。
# 合并模块 *
除了 usedExports 选项之外,我们还可以使用一个 concatenateModules 选项继续优化输出。
普通打包只是将一个模块最终放入一个单独的函数中,如果我们的模块很多,就意味着在输出结果中会有很多的模块函数。
concatenateModules 配置的作用就是尽可能将所有模块合并到一起输出到一个函数中,这样既提升了运行效率,又减少了代码的体积。
我们回到配置文件中,这里我们在 optimization 属性中开启 concatenateModules。同时,为了更好地看到效果,我们先关闭 minimize,具体配置如下:
// ./webpack.config.js
/** @type {import('webpack').Configuration} */
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
minimize: false
}
}
然后回到命令行终端再次运行打包。那此时 bundle.js 中就不再是一个模块对应一个函数了,而是把所有的模块都放到了一个函数中,具体结果如下:

