1. 什么是 Tree Shaking解决什么问题核心思路一句话Tree Shaking 就是在构建阶段分析模块之间的依赖关系和导出使用情况把确定没有被使用、且可以安全删除的代码从最终产物中移除从而减小打包体积。解决方案流程图源代码 │ ▼ ES Module 静态分析 │ ├── 分析 import / export │ ▼ 建立模块依赖图 │ ▼ 判断哪些导出真正被使用 │ ▼ 标记未使用代码 │ ▼ 代码生成 / 压缩 │ ▼ 删除无用代码 │ ▼ 更小的最终 Bundle结构化逻辑主要矛盾最终 Bundle 中包含了运行时根本不会执行的代码导致 JavaScript 体积变大。次要矛盾Webpack 必须能够静态判断“哪些代码可以安全删除”否则贸然删除可能改变程序行为。所以 Tree Shaking 的核心不是简单的“发现没调用的函数 → 删除。”而是静态分析模块依赖和导出使用关系 → 标记未使用代码 → 在满足安全条件的情况下删除。2. 举例说明 Tree Shaking 是怎么工作的例如// math.jsexportfunctionadd(a,b){returnab;}exportfunctionsubtract(a,b){returna-b;}入口文件// main.jsimport{add}from./math.js;console.log(add(1,2));从依赖关系看main.js │ │ import { add } ▼ math.js ├── add ← 被使用 └── subtract ← 没有被使用理论上的 Tree Shaking 结果functionadd(a,b){returnab;}console.log(add(1,2));subtract就没有必要进入最终产物。但是要注意Tree Shaking 并不是看到“函数没有调用”就一定删除。例如exportfunctionfoo(){console.log(foo);}foo();或者exportfunctionfoo(){console.log(foo);}foo();函数本身虽然没有被其他模块导入但模块内部可能执行了它。更典型的是// init.jsconsole.log(初始化程序);exportconstvalue100;即使import./init.js;没有使用任何导出console.log(初始化程序)仍然可能必须保留。因此Tree Shaking 的核心难点不是“找没用的代码”而是判断删除代码是否会改变程序语义。3. Webpack 如何开启 Tree Shaking核心思路一句话Webpack 通过optimization.usedExports标记模块中哪些导出没有被使用再结合压缩器真正删除这些无用代码。典型配置// webpack.config.jsconstTerserPluginrequire(terser-webpack-plugin);module.exports{mode:production,optimization:{// 分析每个模块的导出哪些被使用、哪些没有被使用usedExports:true,// 对最终代码进行压缩。// 压缩器会根据 Webpack 的标记结果删除// 可以安全删除的无用代码。minimize:true,minimizer:[newTerserPlugin(),],},};usedExports负责分析并标记哪些 export 被使用它本身主要负责“标记”真正把代码从产物中删除通常还需要后续的代码压缩阶段。可以理解为usedExports ↓ 告诉 Webpack “这个导出没人用” ↓ 标记未使用代码 ↓ Terser 等压缩器 ↓ 真正删除无用代码4. 为什么 Tree Shaking 通常要求使用 ES Module核心思路一句话因为 ES Module 的import/export具有静态结构Webpack 可以在运行前分析依赖关系CommonJS 的require可以动态执行静态分析难度更高。例如import{add}from./math.js;Webpack 在构建阶段就能知道main.js ↓ 需要 math.js 的 add ↓ subtract 没有被使用而 CommonJSconstnamegetModuleName();constmathrequire(name);运行之前甚至无法确定到底加载哪个模块require(name) │ ▼ name 运行时才能确定 │ ▼ 静态分析困难因此ES Module 静态 import/export ↓ 构建阶段可分析 ↓ 适合 Tree Shaking而CommonJS require() 可以动态执行 ↓ 依赖关系可能运行时才能确定 ↓ Tree Shaking 难度更高但不要绝对化不要在面试中说“CommonJS 完全不能 Tree Shaking。”更准确的说法是Tree Shaking 最适合具有静态依赖关系的 ES Module。Webpack 对 CommonJS 也存在一定的静态分析能力但动态require等场景会限制 Tree Shaking 效果。5. Tree Shaking 和sideEffects是什么关系这是面试中非常容易继续追问的问题。核心思路usedExports解决“哪些导出没有被使用”sideEffects解决“整个模块在没人使用其导出时能不能直接跳过”。例如// polyfill.jsArray.prototype.myMethodfunction(){// ...};然后import./polyfill.js;虽然没有使用任何 exportpolyfill.js ↓ 没有 export 被使用但这个模块执行时会修改全局对象polyfill.js ↓ 修改 Array.prototype ↓ 产生副作用所以不能简单删除。sideEffects配置例如{sideEffects:false}表示这个包中的模块被认为没有副作用如果某个模块的导出没有被使用可以更积极地进行删除。例如{sideEffects:[*.css,./src/polyfill.js]}意思是默认认为模块没有副作用 │ ├── *.css │ ↓ │ 有副作用 │ └── polyfill.js ↓ 有副作用这样可以避免误删 CSS、Polyfill 等必须执行的模块。6.usedExports和sideEffects有什么区别这是一个很好的面试对比题。配置核心作用usedExports分析某个模块的哪些导出被使用sideEffects判断模块整体是否可以在没有使用导出的情况下被跳过minimize对生成代码进行压缩实际删除大量无用代码TerserPluginWebpack 常见的 JavaScript 压缩实现可以用一句话记usedExports → 哪个 export 没用 sideEffects → 整个模块能不能不执行 Terser → 这些已经确定没用的代码能不能删掉7. Tree Shaking 的底层实现原理是什么核心思路一句话Webpack 先基于 ES Module 的静态结构建立依赖图并分析 export 的使用情况再标记未使用代码最后通过压缩阶段进行删除。架构图源代码 │ ▼ Module Parser │ ▼ ┌──────────────────┐ │ 模块依赖关系图 │ └──────────────────┘ │ ▼ 分析 import / export 使用关系 │ ▼ usedExports │ ┌─────────┴─────────┐ │ │ 被使用 未被使用 │ │ ▼ ▼ 保留 标记删除 │ ▼ 代码压缩器 │ ▼ 删除 Dead Code │ ▼ 最终 Bundle为什么叫 Tree Shaking因为模块依赖关系可以抽象成一棵树入口模块 │ ├── A │ ├── A1 │ └── A2 │ └── B ├── B1 └── B2如果入口 └── A └── A1真正使用到的只有A └── A1那么A2 B ├── B1 └── B2如果确定没有副作用就可以从最终产物中消除。8. Tree Shaking 的边界场景有哪些场景一动态模块加载constmoduleNamegetModuleName();constmodulerequire(moduleName);依赖关系运行时才确定构建阶段 ↓ 无法确定 moduleName ↓ 无法完整确定依赖关系 ↓ Tree Shaking 能力受限场景二模块存在副作用// init.jswindow.appInitializedtrue;即使import./init.js;没有使用 export也不能随意删除删除 init.js ↓ window.appInitialized 不再设置 ↓ 程序行为发生变化场景三CSS例如import./style.css;CSS 本身没有 JavaScript export但导入行为会产生实际效果。因此很多项目会{sideEffects:[*.css]}场景四CommonJSconstutilsrequire(./utils);尤其是动态require(./${name}.js);静态分析能力明显受限。9. Tree Shaking 和代码分割有什么区别这个也很容易混淆。Tree Shaking解决“不要的代码删掉。”一个 Bundle ┌──────────────────────┐ │ 使用代码 │ │ 无用代码 ← 删除 │ └──────────────────────┘Code Splitting解决“代码不要全部放在一个 Bundle 中拆成多个 Chunk按需加载。”App │ ├── main.js │ ├── dashboard.js │ └── editor.js例如constEditorawaitimport(./Editor.js);形成首屏 ↓ main.js 用户打开编辑器 ↓ Editor.js ↓ 动态加载所以Tree Shaking → 减少“代码总量” Code Splitting → 减少“单次加载量”两者可以一起使用。10. Tree Shaking 的实际使用场景最典型的是工具库。例如import{debounce}fromsome-utils;如果库本身使用 ES Modulesome-utils ├── debounce ├── throttle ├── cloneDeep ├── merge ├── ...而项目只使用debounce理想情况下debounce ← 保留 throttle ← 删除 cloneDeep ← 删除 merge ← 删除这样可以减少 Bundle 体积。11. Tree Shaking 最佳实践① 优先使用 ES Moduleimport{debounce}from./utils.js;而不是大量使用动态require(moduleName);② 正确声明sideEffects如果你的包确实没有副作用{sideEffects:false}如果存在 CSS、Polyfill 等副作用{sideEffects:[*.css,./src/polyfill.js]}不要为了 Tree Shaking 效果随便写sideEffects: false。否则可能出现错误声明 ↓ Webpack 认为模块无副作用 ↓ 模块被删除 ↓ 初始化代码 / CSS / Polyfill 不执行 ↓ 程序出现异常③ 生产环境构建通常module.exports{mode:production};Webpack 会启用生产环境相关优化包括代码压缩等。因此实际项目一般不需要手工把所有优化开关逐个配置。12. 一个完整的 Tree Shaking 示例项目结构src/ ├── main.js └── math.jsmath.js// math.js// 这个函数会被 main.js 使用。// 因此它应该被保留在最终产物中。exportfunctionadd(a,b){returnab;}// 这个函数没有被 main.js 使用。// 在满足 Tree Shaking 条件的情况下最终产物中可以被删除。exportfunctionsubtract(a,b){returna-b;}// 同样没有被使用也可以被删除。exportfunctionmultiply(a,b){returna*b;}main.js// main.js// 只导入 add。// Webpack 可以通过静态分析知道// math.js 中只有 add 是当前模块需要使用的导出。import{add}from./math.js;// 使用 add。console.log(add(10,20));webpack.config.jsconstpathrequire(path);module.exports{// Webpack 的入口文件。entry:./src/main.js,// 使用生产模式。// 生产模式会启用一系列默认优化包括代码压缩。mode:production,output:{// 输出文件名。filename:bundle.js,// 输出目录。path:path.resolve(__dirname,dist),// 每次构建前清理 dist。clean:true,},optimization:{// 分析 ES Module 的 export 使用情况。//// 例如// add → 被使用// subtract → 未使用// multiply → 未使用//// Webpack 会对这些信息进行标记。usedExports:true,// 开启最终代码压缩。//// 注意// usedExports 主要负责“分析和标记”// 压缩器负责进一步删除已经确定无用的代码。minimize:true,},};最终可以理解为math.js │ ├── add ──────→ 使用 → 保留 ├── subtract ──────→ 未使用 → 标记 → 删除 └── multiply ──────→ 未使用 → 标记 → 删除13. Tree Shaking 最容易被面试官追问的几个问题问题 1Tree Shaking 是不是 Webpack 独有不是。Tree Shaking 是一种无用代码消除思想现代前端构建工具普遍支持类似能力例如Webpack Rollup Rspack esbuild 其他现代 JavaScript 构建工具具体实现机制和优化程度可能不同。问题 2usedExports: true就等于 Tree Shaking 吗不能简单画等号。更准确usedExports → 分析 export 使用情况 → 标记未使用 export 代码压缩器 → 根据标记删除 Dead Code所以完整理解应该是Tree Shaking 是一套无用代码消除机制usedExports是 Webpack 实现其中“使用情况分析”的重要配置。问题 3为什么开发环境看到没删除因为开发环境 → 更关注构建速度、调试体验 → 不一定进行完整压缩 生产环境 → 更关注最终 Bundle 体积 → 通常进行 Tree Shaking Minification所以不要仅仅看开发环境输出文件判断 Tree Shaking 是否有效。问题 4Tree Shaking 能不能删除所有没有执行的代码不能。核心限制就是不能在无法证明“删除后不会改变程序行为”的情况下随意删除代码。尤其需要关注副作用 动态依赖 CommonJS 全局变量修改 原型修改 模块初始化 CSS Polyfill14. 这道题的真正考点可以把整道题压缩成四层第一层概念 Tree Shaking 消除无用代码 ↓ 第二层为什么能做 ES Module 的 import/export 具有静态结构 ↓ 第三层Webpack 怎么做 usedExports → 分析 export 使用情况 → 标记未使用代码 ↓ 第四层为什么能安全删除 sideEffects 静态依赖分析 代码压缩 ↓ 最终 减少 Bundle 体积主要矛盾如何准确判断哪些代码既没有被使用又可以安全删除。次要矛盾usedExports、sideEffects、压缩器分别承担什么职责以及 ES Module / CommonJS 对静态分析能力的影响。满分答案面试题Webpack 如何实现 Tree Shaking核心思路Tree Shaking 就是在构建阶段利用 ES Module 的静态结构分析模块依赖和 export 的使用情况标记未使用代码再结合副作用分析和代码压缩把可以安全删除的无用代码从最终 Bundle 中移除。流程图ES Module 源代码 │ ▼ 分析 import / export │ ▼ 建立模块依赖关系 │ ▼ 分析 export 使用情况 │ ▼ usedExports 标记未使用代码 │ ├── 有副作用 ──→ 不能随意删除 │ └── 无副作用 ──→ 可以进入删除阶段 │ ▼ Terser 等压缩器 │ ▼ 删除无用代码 │ ▼ 更小的 Bundle底层原理例如// math.jsexportfunctionadd(a,b){returnab;}exportfunctionsubtract(a,b){returna-b;}// main.jsimport{add}from./math.js;console.log(add(1,2));Webpack 能在构建阶段静态分析main.js │ └── 使用 math.js 的 add math.js ├── add → 使用 → 保留 └── subtract → 未使用 → 标记 → 压缩阶段删除Webpack 中optimization:{usedExports:true,minimize:true,}其中usedExports主要负责分析和标记哪些 export 被使用而代码压缩器负责真正删除能够安全删除的代码。为什么 ES Module 更适合 Tree Shaking因为import{add}from./math.js;属于静态模块依赖构建阶段就可以知道依赖关系。而constnamegetModuleName();require(name);模块在运行时才能确定静态分析能力会受到限制。还要注意副作用例如// polyfill.jsArray.prototype.foofunction(){};即使没有使用任何 export这个模块执行本身也会修改全局对象因此不能简单删除。所以还需要正确配置{sideEffects:false}或者明确声明有副作用的文件{sideEffects:[*.css,./src/polyfill.js]}这里最容易犯的错误是把usedExports理解成“直接删除无用代码”。更准确地说usedExports负责使用情况分析和标记最终的代码删除通常由压缩阶段完成。和代码分割的区别Tree Shaking → 删除不需要的代码 → 减少代码总量 Code Splitting → 把代码拆成多个 Chunk → 减少单次加载量两者可以配合使用。一句话总结Webpack Tree Shaking 的本质就是利用 ES Module 的静态依赖关系分析哪些导出没有被使用再结合副作用分析和代码压缩把确定无用的代码从最终 Bundle 中删除从而减少 JavaScript 体积。