创见博客
webpack Tree Shaking:摇掉没用的代码
七崽爱吃小饼干2026/09/16阅读 0专栏 webpack构建高级

打包产物里最让人心疼的,往往是那些写了却从没被用到的代码:工具库里导出几十个函数,你只用了两个;组件库提供上百个组件,你只引了一个。它们被一起打进 bundle,白白增加体积。**Tree Shaking(摇树优化)**就是把这些「死代码」摇掉。

一、为什么需要 Tree Shaking

先看一个例子:

js
// src/utils.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}
js
// src/index.js
import { add } from './utils.js';

console.log(add(1, 2));

这里只用了 add,subtract 和 multiply 从头到尾没被引用。但在没有 Tree Shaking 的情况下,打包产物会把三个函数全部带上——因为打包器默认「导入即引入整个模块」。

问题在于:

  • 工具库:lodash 有几百个方法,全量引入动辄几十上百 KB。
  • 组件库:只用一两个组件,却把整包代码都带进来。
  • 项目自身:历史遗留的未使用函数、常量也会一直留在产物里。

在 CommonJS 时代,这个问题尤其难解。因为 require 是运行时行为,module.exports 可以动态赋值,打包器在构建时无法确定「哪些导出真的被用了」,也就无法安全删除。

二、Tree Shaking 是什么

Tree Shaking 指的是:

基于 ES Module 的静态结构,在构建时分析出哪些导出被真正使用,把未被使用的代码从产物中移除。

它属于**死代码消除(Dead Code Elimination)**的一种,但因为作用在「模块导出的粒度」上,常被单独拿出来讲。

一个常见的误解是:Tree Shaking 是 webpack 一个开关。实际上它是多个环节配合的结果:

  1. 打包器(webpack / Rollup):基于 ESM 的静态分析,标记哪些导出未被使用(usedExports),哪些模块可以跳过(sideEffects)。
  2. 压缩器(Terser):根据标记,真正把无用的代码删除。
  3. 模块规范(ESM):提供可静态分析的前提。

也就是说,webpack 负责「标记」,Terser 负责「删除」。只开启其中一个,效果都不完整。

三、Tree Shaking 的前提与原理

1. 必须使用 ESM

Tree Shaking 的基础是 ES Module 的静态结构:import / export 只能写在顶层,导入导出关系在代码运行前就能确定。

js
// 可以被 Tree Shaking(静态)
import { add } from './utils.js';
export const foo = 1;

而 CommonJS 是动态的,无法可靠分析:

js
// 难以 Tree Shaking(动态)
const name = 'add';
const fn = require('./utils')[name];
module.exports = computeExports();

所以第一条铁律:用 ESM 写代码。

2. 打包器标记 usedExports

webpack 会构建模块依赖图,分析每个模块导出了什么(providedExports)、哪些被别处引用(usedExports),并给未使用的导出打上标记。

3. sideEffects 决定能否整块跳过

usedExports 只能删除「模块里未使用的导出」。但有些模块本身没有导出,或导入它只是为了触发副作用(比如引入 polyfill、引入 CSS)。webpack 需要知道:这个模块被 import 时,是否只是「顺便引入」,能不能在没用到时直接跳过?

这就是 package.json 里 sideEffects 字段的作用:

json
{
  "sideEffects": false
}

表示「本包所有文件都没有副作用,未使用即可安全删除」。如果只有部分文件有副作用,用数组列出:

json
{
  "sideEffects": ["*.css", "./src/polyfill.js"]
}

4. 顶层副作用会阻止摇树

只要模块在顶层有副作用(修改全局变量、调用函数等),打包器就不敢轻易删除:

js
// 难以摇掉
export function add(a, b) { return a + b; }
window.foo = 'bar'; // 顶层副作用

而纯函数、纯常量则很容易被标记和删除。

四、怎么用

1. 生产模式已自动开启

webpack 在 mode: 'production' 下会默认启用 Tree Shaking 相关选项:

js
module.exports = {
  mode: 'production',
};

它自动打开了 usedExports、sideEffects、minimize 等。开发模式下为了构建速度和调试方便,这些默认是关闭的。

2. 手动配置(开发环境调试时)

js
module.exports = {
  mode: 'development',
  optimization: {
    usedExports: true,
    sideEffects: true,
  },
};

注意:开发模式下 minimize 通常为 false,所以「标记」有了,但代码不会真正被删——这是正常的,要看最终效果请用生产构建。

3. 在 package.json 声明 sideEffects

对业务项目:

json
{
  "name": "my-app",
  "sideEffects": ["*.css", "*.less", "./src/polyfill.js"]
}

对纯函数工具库:

json
{
  "name": "my-utils",
  "sideEffects": false
}

4. 关键坑:别让 Babel 把 ESM 转成 CommonJS

这是 Tree Shaking 失效最常见的原因。@babel/preset-env 的 modules 选项如果被设成 'commonjs',ESM 会先被 Babel 转成 require,webpack 再拿到就已经是 CommonJS 了,Tree Shaking 直接失效。

js
// 错误:会破坏 Tree Shaking
{
  presets: [['@babel/preset-env', { modules: 'commonjs' }]]
}

// 正确:交给 webpack 处理模块
{
  presets: [['@babel/preset-env', { modules: false }]]
}

modules: false 让 Babel 保留 ESM 语法,把模块处理交给 webpack。默认值 'auto' 在 webpack 环境下通常也会保持 ESM,但显式写 false 最稳妥。

5. 注意 CSS 等副作用资源

如果 sideEffects: false,而项目里又通过 import './style.css' 引入样式,webpack 可能认为「这个模块没有导出,也没有副作用」,从而把样式整个删掉。

所以有样式的项目一定要把 CSS 列进 sideEffects:

json
{
  "sideEffects": ["*.css", "*.less", "*.scss"]
}

五、如何验证 Tree Shaking 生效

  1. 看产物:搜索未使用的函数名,确认是否还在 bundle 里。
  2. webpack 统计信息:
bash
npx webpack --mode production --json > stats.json

从中查看 usedExports、各模块的体积变化。

  1. webpack-bundle-analyzer:可视化产物体积,直观看到哪些包占用大:
bash
npm install webpack-bundle-analyzer --save-dev
js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [new BundleAnalyzerPlugin()],
};
  1. Chrome Coverage:在 DevTools 的 Coverage 面板中查看代码实际执行覆盖率,辅助判断。

六、完整配置示例

js
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    clean: true,
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            // 保留 ESM,交给 webpack 做 Tree Shaking
            presets: [['@babel/preset-env', { modules: false }]],
          },
        },
      },
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  optimization: {
    // production 下默认已开启,这里显式写出便于理解
    usedExports: true,
    sideEffects: true,
    minimize: true,
    // 作用域提升,把模块合并进同一作用域,摇树更彻底
    concatenateModules: true,
  },
};

对应的 package.json:

json
{
  "sideEffects": ["*.css", "*.less"]
}

七、常见失效原因

现象原因解决
未使用导出仍在产物里用了 CommonJS改用 ESM
同上Babel 转成了 require设 modules: false
同上开发模式未压缩用 mode: 'production'
整个模块删不掉顶层有副作用抽离副作用、声明 sideEffects
样式被删掉sideEffects: false 误伤把 CSS 列入 sideEffects 数组
库的代码摇不掉该库未声明 sideEffects反馈给库作者,或改用按需引入

小结

Tree Shaking 的本质,是「基于 ESM 静态分析的死代码消除」,它的落地依赖三件事:

  1. 写 ESM:用 import / export,不用 require。
  2. 让 webpack 标记、让 Terser 删除:生产模式下自动完成。
  3. 如实声明副作用:用 package.json 的 sideEffects 帮打包器判断哪些模块可以安全跳过。

最容易踩的两个坑是:Babel 把 ESM 转成了 CommonJS,以及 sideEffects: false 误删了 CSS。避开它们,Tree Shaking 就能稳定发挥作用,帮你显著瘦身产物体积。

评论
0/100