Skip to content

Tree Shaking 不是魔法,是有代价的

提示本文发布于 5 个月前,其中信息可能已经时过境迁...

以前我一直觉得 Tree Shaking 是打包器自动做的事,写代码的时候不太需要考虑。直到有一次用 bundle analyzer 看打包结果,发现一个只用了两个方法的工具库,竟然把整个文件都打包进去了。

查了一圈才明白,Tree Shaking 不是魔法,它有很多前提条件——写不对,它就摇不动。

核心原理:静态分析

Tree Shaking 依赖 ESM 的静态结构。import { add } from './math' 这种写法,打包器在编译时就能确定你用了什么、没用什么。

但 CommonJS 就不行:

javascript
// ❌ 动态的,摇不动
const math = require('./math');
const { add } = math;

// ✅ 静态的,可以摇
import { add } from './math';

几个常见陷阱

1. 默认导出对象

javascript
// ❌ 导出一个对象,里面的方法全都会被打包
export default {
  add: (a, b) => a + b,
  multiply: (a, b) => a * b,
};

// ✅ 命名导出,只打包用到的
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;

2. 有副作用的代码

如果函数里有 console.log,打包器会认为它有副作用,不敢移除:

javascript
// 即使没人调用 multiply,它也会被打包
export function multiply(a, b) {
  console.log('multiply called');
  return a * b;
}

可以通过 /*#__PURE__*/ 注释告诉打包器这是安全的:

javascript
export function multiply(a, b) {
  /*#__PURE__*/ console.log('multiply called');
  return a * b;
}

3. import * 的问题

javascript
// ❌ 整个模块都会被打包
import * as utils from './utils';
utils.add(1, 2);

// ✅ 只打包 add
import { add } from './utils';

第三方库怎么选

  • lodash → 用 lodash-es 或者按路径导入 import debounce from 'lodash/debounce'
  • moment → 换成 date-fns,天然支持 Tree Shaking
  • UI 组件库 → 确认是否支持按需导入

调试打包体积

webpack-bundle-analyzer 或者 Vite 的 rollup-plugin-visualizer,能直观看到每个模块的大小和引用关系。我每次重构完都会跑一遍,看看有没有意外打包进去的东西。

Tree Shaking 这件事,说到底就是:写好 ESM、避免副作用、选对库版本。不是什么高深的技术,但忽略了它,打包体积就会不知不觉地涨上去。