Tree Shaking 去除无用代码:依赖优化与 ESM

FreeGuideOnline 最新 2026-06-15

什么是 Tree Shaking?

Tree Shaking 是一个在前端构建过程中**移除未引用代码(Dead Code)**的优化技术。它的名字来源于想象一棵依赖树,摇晃掉枯黄的叶子(未使用的导出)。通过 Tree Shaking,最终的打包文件可以显著减小,从而加快页面加载速度。

它通常由模块打包器(如 Webpack、Rollup、esbuild)在生产模式下自动执行。核心前提是代码必须采用 ES Modules (ESM) 的静态语法。

为什么必须使用 ES Modules?

Tree Shaking 依赖 编译时的静态结构分析,而 ES Modules 正好提供了这样的能力:

  • importexport 声明是静态的,不能放在条件语句或动态逻辑中。
  • 导入/导出关系在代码执行前就可以确定,打包工具可以安全地分析出哪些导出被真正使用。

相反,CommonJSrequire / module.exports)是动态的,导出对象可以在运行时修改,因此无法在构建阶段进行可靠的未引用代码消除。如果你的项目仍有 CommonJS 模块,它们会被完整打包,成为 Tree Shaking 的盲区。

// 静态导入 — 支持 Tree Shaking
import { add } from './math.js';

console.log(add(1, 2));
// 动态导入(条件)— 无法被 Tree Shaking
const modulePath = './math.js';
const { add } = await import(modulePath);

Tree Shaking 的工作原理

  1. 标记导出使用情况
    打包工具从入口文件开始,追踪所有 import 语句,并构建一个模块依赖图。对于每个模块,检查哪些 export 被实际引用。

  2. 擦除未使用的导出
    对于未被任何模块引用的导出函数、类、变量,打包工具会将其从产物中删除(或者标记为无副作用且安全删除)。

  3. 消除副作用(Side Effects)的影响
    如果一个模块在被导入时执行了会影响全局状态的代码(例如修改原型、设置全局变量),即使它的导出未被使用,也可能不能直接删除。这时需要通过 package.jsonsideEffects 字段明确声明模块是否副作用。

如何在实际项目中启用 Tree Shaking

Webpack 配置示例

Webpack 在 mode: 'production' 下默认启用 Tree Shaking。你也可以在开发配置中显式开启:

// webpack.config.js
module.exports = {
  mode: 'production', // 生产模式,自动启用
  optimization: {
    usedExports: true, // 标记未使用的导出
    minimize: true     // 使用 Terser 等工具删除死代码
  }
};

Rollup / Vite 默认行为

Rollup 天生围绕 ESM 构建,对 Tree Shaking 的支持更加彻底。Vite 在生产构建时基于 Rollup,因此也自动获得 Tree Shaking 能力,无需额外配置。

# Vite 生产构建直接输出优化后的结果
npm run build

package.json 中的 sideEffects 字段

这是 Tree Shaking 中最关键但又容易被忽略的配置。它告诉打包工具:“哪些文件即使导出未被使用,也可能需要保留,因为它们包含副作用”
无副作用的模块才能被安全删除。

// 对于所有文件都无副作用(推荐)
"sideEffects": false
// 仅标记某些文件有副作用(如全局 CSS 导入)
"sideEffects": [
  "*.css",
  "*.scss",
  "./src/polyfills.js"
]

⚠️ 如果你的库或应用中有全局风格文件(如 import './styles.css'),一定要在 sideEffects 中声明,否则这些文件会被 Tree Shaking 移除。

开发库时的配置

如果你正在开发一个 npm 包,为了让使用方能够 Tree Shaking,必须提供包含 module 字段指向的 ESM 产物,并在 exportsmain 之外声明。

{
  "name": "my-library",
  "version": "1.0.0",
  "main": "lib/index.cjs",    // CommonJS 回退
  "module": "es/index.js",    // ESM 入口(支持 Tree Shaking)
  "sideEffects": false,
  "files": ["lib", "es"]
}

编写可被 Tree Shaking 优化的代码

  • 使用 ESM 导入/导出import { specificFunc } from './utils' 而不是 import * as utils from './utils'(后者会导入整个模块对象,使得打包工具难以剔除内部成员)。
  • 避免产生“副作用”导出:不要在一个模块的顶级作用域中执行影响外部的操作(如修改数组原型)。将初始化逻辑抽离成显式调用的函数。
  • 保持函数和类单一职责:方便按需导入,不要把所有工具都塞进一个巨大对象。
  • 提供副作用标记:在 package.json 中正确设置 sideEffects,以保证全局样式或 polyfill 不被意外删除。

反例与正例

// ❌ 不易 Tree Shaking — 导入整个模块并执行副作用
import './polyfills';
import * as utils from './helpers';

utils.doA();
utils.doB();

// ✅ 便于 Tree Shaking — 按需导入,polyfill 在 sideEffects 中声明
import { doA } from './helpers';

doA();

常见问题与调试方法

1. 为什么打包后体积仍然很大?

  • 检查是否有 CommonJS 依赖,如 lodash 老版本。可通过 node_modules/lodashpackage.json 确认。换用 lodash-es 或按路径导入 lodash/debounce
  • 运行打包分析工具:webpack-bundle-analyzer 或 Rollup 的可视化插件,查看哪些模块未被摇掉。

2. 副作用被意外保留导致 Tree Shaking 失效

一个模块哪怕只有一个顶层副作用(例如 console.log('init')),打包工具为了安全会保留整个模块。将这些代码迁移到函数内部,或在 sideEffects 中精确标记。

3. CSS / 样式文件消失

必须把 .css.scss 等文件路径添加到 sideEffects 数组中,否则它们会被视为无副作用的模块而删除。

总结

  • Tree Shaking 依赖 ES Modules 的静态特性,CommonJS 是主要障碍。
  • 不仅需要打包工具(Webpack / Rollup)开启优化,更要通过 sideEffects 声明在模块级别消除不确定性。
  • 日常开发中采用按需导入、避免顶层副作用,能让最终产物体积得到最大程度的瘦身。
  • 利用打包分析工具定期审查输出,确保你的“树叶”确实被摇掉了。

现在你可以立刻检查你的项目:是否正确配置了 sideEffects?是否还有未迁移的 CommonJS 模块?处理完这些,你的前端包将轻快许多。