深度解析 Tree-Shaking:从入门到究极理解

一、为什么要关心 Tree-Shaking?

想象一下你在写一个电商项目,随手引入了一个工具库:

js 复制代码
import { add } from "math-toolkit";

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

这个库很大,里面还有 multiplydividesqrt 等几十个函数。你只用到了一个 add,但传统打包工具还是会把整包代码打进 bundle。结果呢?

👉 你的用户要白白下载几百 KB 的没用代码。

这就好比你去超市买了一瓶水,收银员却让你把整车货推回家。

Tree-Shaking 就是来解决这个问题的:只"摇"下你真正用到的代码,把没用的部分抖掉。


二、Tree-Shaking 的基本原理

Tree-Shaking 基于 ES Module 的静态分析特性

也就是说,打包工具在构建阶段就能看出来哪些导出函数/变量被使用了,哪些完全没用。

比如:

js 复制代码
// utils.js
export function add(a, b) {
  return a + b;
}

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

// index.js
import { add } from './utils.js';

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

构建结果只会保留 add,而 minus 会被摇掉。


三、副作用与 sideEffects

事情没这么简单。Tree-Shaking 并不是只看"有没有用到",还得考虑 副作用(side effects)

什么是副作用?就是某段代码即使你没用导出的东西,也会"顺带干点别的事"。

比如:

js 复制代码
// utils.js
console.log("hello, I run anyway!");
export function add(a, b) {
  return a + b;
}

哪怕你只是写了:

js 复制代码
import './utils.js';

这个 console.log 依然会执行,因为文件顶层代码就是副作用。

Webpack 为了处理这种情况,引入了一个字段:

json 复制代码
{
  "sideEffects": false
}
  • false:表示整个包都没有副作用,可以随便摇。

  • 数组:指定哪些文件有副作用,例如:

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

这样打包器就知道:大部分文件都能安全摇掉,但 polyfill 和样式表必须保留。


四、顶层副作用的执行时机

很多人会有个疑问:

如果我 import './utils.js',而且在 package.json 里设置了 sideEffects: false,那它的副作用什么时候执行呢?是不是执行完再被删掉?

关键点来了:

  • 执行发生在浏览器运行时,而不是打包阶段。
  • 删除发生在构建阶段

换句话说,如果 Webpack 确认某个模块没有被用到,并且 sideEffects 声明它也没副作用,它在最终 bundle 里根本不会出现,自然也就不会有机会执行。

但如果 Webpack 觉得"这玩意儿可能有副作用",它会保留 import './utils.js' 对应的内容,哪怕你没用导出的函数。

所以,副作用是构建阶段就决定是否保留的,而不是运行完再删


五、Terser 在其中扮演什么角色?

Webpack 本身只做"标记和剪枝",真正把"没用的代码删除掉"的工作,很多时候交给 Terser 这种压缩工具。

流程大概是这样的:

  1. Webpack 分析依赖树:知道哪些导出没被用。
  2. 标记未使用代码 :例如打上 /* unused harmony export ... */
  3. Terser 压缩阶段:发现这些标记的代码不会被引用,于是直接删掉。

换句话说,Tree-Shaking 是一个"协作过程":

  • Webpack 做"分析和标记";
  • Terser 做"实际删除"。

如果你只用 Webpack 而没开 Terser,很多"没用的函数"可能还会留在 bundle,只是"永远不会被调用"。


六、举个完整例子

假设你写了:

js 复制代码
// math.js
export function add(a, b) { return a + b; }
export function minus(a, b) { return a - b; }
console.log("math module loaded");

// index.js
import { add } from './math.js';
console.log(add(1, 2));

情况 1:sideEffects: true

最终产物会保留:

  • add(因为用到了)
  • console.log("math module loaded")(顶层副作用必须执行)
  • minus(没用到,会被标记并在 Terser 阶段删掉)

情况 2:sideEffects: false

Webpack 认为 math.js 没副作用,于是直接摇掉 console.log("math module loaded"),只保留 add

👉 结果比情况 1 更干净。


七、常见坑点

  1. import 'xxx' 裸引入
    如果 xxx 文件里有副作用(比如注册 polyfill),它一定会执行,除非你明确告诉 Webpack 它没副作用。
  2. CommonJS 不友好
    Tree-Shaking 基于 ESM 的静态分析,require() 是动态的,优化效果不如 import
  3. 副作用写法要注意
    比如 Array.prototype.myMethod = ... 这种改全局对象的写法,Tree-Shaking 永远不会删,因为它无法保证安全。

八、最佳实践总结

  • 优先使用 ESM(import/export),不要用 CommonJS。
  • package.json 里正确设置 sideEffects
  • 不要在工具函数里乱写副作用。
  • 配合 Terser 等压缩器,才能看到最终瘦身效果。

九、心智模型:Tree-Shaking 本质

Tree-Shaking 不是"执行完再删",而是:

  • 构建阶段:标记哪些东西没用 / 哪些有副作用必须保留。
  • 压缩阶段:Terser 之类的工具把没用的代码物理删除。
  • 运行阶段:浏览器执行的只有最终保留下来的代码。

所以可以把它想象成:

👉 打包工具提前帮你把代码"筛干净",让浏览器只拿到真正需要的那一部分。


十、结语

Tree-Shaking 看似只是"删除无用代码",但里面牵扯到 ESM 静态分析副作用标记压缩器配合 等多个环节。

理解了副作用执行时机、sideEffects 的作用,以及 Terser 的角色,你就能做到真正的 终极理解

所以,下次有人问你 Tree-Shaking 是啥,你可以自信地说:

它不是"代码运行后再清理垃圾",而是"在打包阶段提前分析,把没用的东西丢掉,只把干净的代码交给浏览器"。

相关推荐
EndingCoder几秒前
安装与环境搭建:准备你的 Electron 开发环境
前端·javascript·electron·前端框架
蓝银草同学16 分钟前
前端离线应用基石:深入浅出 IndexedDB 完整指南
前端·indexeddb
龙在天27 分钟前
什么是SourceMap?有什么作用?
前端
雪中何以赠君别31 分钟前
Vue 2 与 Vue 3 双向绑定 (v-model) 区别详解
前端·javascript·vue.js
林太白33 分钟前
Vue3-ElementPlus使用
前端·javascript·vue.js
Juchecar1 小时前
npm、pnpm、yarn 是什么?该用哪个?怎么用?如何迁移?
前端·node.js
CYRUS_STUDIO1 小时前
Miniconda 全攻略:优雅管理你的 Python 环境
前端·后端·python
学不动学不明白1 小时前
ECharts 为visualMap视觉映射添加自适应外边框
前端
怪可爱的地球人1 小时前
ts的高级类型
前端
支撑前端荣耀1 小时前
优雅的Git提交:用Husky为你的项目加上提交约束
前端·javascript