13、webpack treeShaking机制的原理

张开发
2026/4/11 6:19:32 15 分钟阅读

分享文章

13、webpack treeShaking机制的原理
目录一、先建立认知Tree Shaking 是什么二、核心原理为什么 Tree Shaking 依赖 ES ModuleCommonJS 为什么不支持ES Module 为什么可以面试亮点说法三、Tree Shaking 的实现流程第一阶段标记Mark第二阶段清除Shake面试亮点说法四、sideEffects 是什么为什么重要问题场景什么是副作用Side Effects解决方案sideEffects 字段有了这个声明之后面试亮点说法五、Tree Shaking 失效的常见场景1. 使用了 CommonJS2. Babel 把 ESM 转成了 CommonJS3. 没开启生产模式4. 导出的是对象或类5. 动态 import 里的内容面试亮点说法六、面试回答精彩模板版本一标准版1 分钟版本二进阶版更有深度七、整体知识结构梳理八、高分总结这道题很多人能说出去掉没用的代码但真正被追问为什么能做到、怎么做到的时就答不上来了。这道题的核心在于你要能讲清楚背后的静态分析原理而不只是说有这个功能。一、先建立认知Tree Shaking 是什么Tree Shaking 这个名字来源于一个形象的比喻把代码依赖关系想象成一棵树摇一摇这棵树没有被抓住的枯叶死代码就会掉落被清除掉。本质在打包阶段把那些被引入但从未被使用的代码从最终产物里删掉减小 bundle 体积。二、核心原理为什么 Tree Shaking 依赖 ES Module这是面试最核心的考点。CommonJS 为什么不支持CommonJS 是动态导入的// 运行时才知道要 require 什么 const mod require(someVariable) // 运行时才知道用哪个导出 if (condition) { const { fn } require(./utils) }require的路径可以是变量导出对象可以在运行时随意修改// CommonJS 可以动态修改导出 module.exports.newFn function() {}由于 CommonJS 的依赖关系只有在运行时才能确定打包工具在编译阶段无法分析哪些代码被用了、哪些没用所以无法做 Tree Shaking。ES Module 为什么可以ES Module 是静态导入的// 必须写在顶层不能在条件语句里 import { fn } from ./utils // 导出也是静态声明 export function fn() {} export const name helloES Module 有三个关键特性让静态分析成为可能import语句只能写在模块顶层不能在函数体、条件语句里动态执行import的路径必须是字符串字面量不能是变量导出的绑定是只读的不能在运行时随意增删这意味着打包工具在编译阶段就能 100% 确定这个模块导出了什么哪些导出被其他模块引用了哪些导出从来没被引用过然后把没被引用的代码标记为未使用在压缩阶段删掉。面试亮点说法Tree Shaking 能工作的根本原因是 ES Module 的静态结构。ESM 的import/export必须在顶层声明、路径必须是字符串字面量这让 Webpack 在编译阶段就能构建出完整的模块依赖图标记出哪些导出从未被使用。而 CommonJS 是动态的require路径可以是变量依赖关系只有运行时才能确定所以不支持 Tree Shaking。三、Tree Shaking 的实现流程Webpack 的 Tree Shaking 分两个阶段完成第一阶段标记MarkWebpack 构建模块依赖图时会分析每个模块的导入导出关系追踪每一个export是否被其他模块import引用对从未被引用的export打上unused harmony export标记这个过程叫做usedExports使用导出分析// optimization 配置 optimization: { usedExports: true // 生产模式默认开启 }打包后你可以在产物里看到类似这样的注释/* unused harmony export unusedFn */第二阶段清除Shake打标记只是告诉压缩器这段代码没用真正删除是靠Terser压缩器。Terser 在压缩阶段会识别这些未使用标记把对应的代码彻底删掉。所以 Tree Shaking 需要两者配合Webpack 负责标记哪些代码未被使用Terser 负责把它们从产物中删除。面试亮点说法Tree Shaking 不是一步完成的是分两阶段Webpack 先做静态分析给未使用的 export 打标记然后 Terser 在压缩阶段识别这些标记把对应代码删掉。所以如果只开启了usedExports但没有开启代码压缩Tree Shaking 是不会真正生效的。四、sideEffects是什么为什么重要这是 Tree Shaking 里经常被忽略但面试非常爱问的点。问题场景假设有这样一个工具库// utils/index.js export function fn1() { /* ... */ } export function fn2() { /* ... */ } export function fn3() { /* ... */ }你只使用了fn1import { fn1 } from ./utils按道理 Webpack 应该能 Tree Shaking 掉fn2和fn3。但如果这个模块里还有一些副作用代码比如修改全局变量、注入 polyfill、注册事件Webpack 就不敢随便删了——因为它不确定这些副作用代码是不是必要的。什么是副作用Side Effects副作用代码是指执行时会对外部环境产生影响的代码比如// 修改全局对象 window.myLib {} // 注入 polyfill Array.prototype.myMethod function() {} // 直接执行的逻辑不是导出的函数 console.log(模块加载了) // CSS 文件 import ./global.css这类代码 Webpack 无法判断要不要保留所以默认会保守处理不删。解决方案sideEffects字段在package.json里明确声明这个包是否有副作用// 整个包都没有副作用可以放心 Tree Shaking { sideEffects: false }// 只有这些文件有副作用其他可以摇 { sideEffects: [ *.css, ./src/polyfill.js ] }有了这个声明之后Webpack 就能大胆摇掉那些引入了但从未被使用的模块而不用担心误删副作用。面试亮点说法sideEffects字段是 Tree Shaking 效果的重要保障。如果不配置Webpack 会保守地保留所有被引入的模块即使你只用了里面一个函数。配置sideEffects: false后Webpack 就知道这个包没有副作用可以放心地把未使用的模块整体摇掉而不只是摇未使用的 export。这对于 UI 组件库、工具函数库的按需引入优化非常关键。五、Tree Shaking 失效的常见场景这几个坑面试说出来很加分1. 使用了 CommonJS// 这种写法 Tree Shaking 不生效 const { fn } require(./utils)原因CommonJS 是动态的Webpack 无法静态分析。2. Babel 把 ESM 转成了 CommonJS// .babelrc { presets: [[babel/preset-env, { modules: commonjs }]] }如果 Babel 把import/export转成了require/module.exports那 Webpack 就拿到了 CommonJSTree Shaking 自然失效。解决方案{ presets: [[babel/preset-env, { modules: false }]] }告诉 Babel 不要转换 ESM保留import/export给 Webpack 处理。3. 没开启生产模式Tree Shaking 的usedExports分析默认只在mode: production下开启。开发模式为了保留完整调试信息不会删代码。4. 导出的是对象或类// 导出对象Tree Shaking 效果有限 export default { fn1() {}, fn2() {}, fn3() {} } // 导出具名函数效果更好 export function fn1() {} export function fn2() {} export function fn3() {}具名导出比默认导出对象更利于 Tree Shaking因为 Webpack 可以更精准地追踪每个导出的使用情况。5. 动态 import 里的内容// 动态路径Webpack 无法静态分析 const module await import(./modules/${name})动态路径无法在编译阶段确定Tree Shaking 自然无效。面试亮点说法Tree Shaking 失效最常见的原因是 Babel 的配置问题。如果babel/preset-env的modules没有设置为falseBabel 会把 ESM 转成 CommonJSWebpack 就看不到import/export了Tree Shaking 就完全失效。这个坑在实际项目里很容易踩到。六、面试回答精彩模板版本一标准版1 分钟Tree Shaking 的核心原理是基于 ES Module 的静态结构。因为 ESM 的import/export必须在顶层声明、路径必须是字面量Webpack 在编译阶段就能构建完整的依赖图分析哪些导出从未被使用打上未使用标记再由 Terser 在压缩阶段删掉。CommonJS 是动态的依赖关系运行时才确定所以不支持 Tree Shaking。实际使用时还需要注意几点Babel 不能把 ESM 转成 CommonJS要在package.json里配置sideEffects告诉 Webpack 哪些文件没有副作用这样 Webpack 才敢把未使用的模块整体摇掉另外生产模式下才默认开启usedExports分析。版本二进阶版更有深度Tree Shaking 能工作的根本是 ES Module 的编译时静态结构。ESM 规定import/export只能在顶层、路径只能是字符串字面量这让 Webpack 在编译阶段就能 100% 确定模块间的依赖关系构建出精确的模块依赖图。具体实现分两步第一步 Webpack 做usedExports分析给所有未被引用的export打上标记第二步 Terser 在压缩时识别这些标记把对应代码从产物里删掉。所以 Tree Shaking 是 Webpack 和 Terser 配合完成的少了任何一环都不会真正生效。还有一个容易被忽视的关键是sideEffects配置。usedExports只能摇掉未使用的具名导出但如果一个模块整体被引入了Webpack 会保守地保留它因为不确定有没有副作用。配置sideEffects: false后Webpack 才能放心地把整个未使用的模块摇掉这对组件库的按需引入优化效果非常明显。常见的失效场景有Babel 把 ESM 转成 CommonJS、使用了 CommonJS 的require、导出的是对象而不是具名 export。其中 Babel 配置问题是实际项目里最容易踩的坑需要把modules设置为false保留 ESM 给 Webpack 处理。七、整体知识结构梳理Tree Shaking 原理 │ ├── 前提ES Module 静态结构 │ ├── import/export 必须在顶层 │ ├── 路径必须是字符串字面量 │ └── 编译阶段即可确定依赖关系 │ ├── 实现两阶段 │ ├── WebpackusedExports 分析 → 标记未使用 export │ └── Terser压缩阶段 → 删除带标记的代码 │ ├── sideEffects 配置 │ ├── false → 整个包无副作用可整体摇模块 │ └── 数组 → 指定有副作用的文件其余可摇 │ └── 常见失效场景 ├── Babel 把 ESM 转成 CommonJS ├── 使用 require 动态导入 ├── 未开启生产模式 ├── 导出对象而非具名 export └── 动态 import 路径八、高分总结Tree Shaking 的本质是利用 ES Module 的静态结构在编译阶段分析模块依赖标记未使用的代码再由压缩器删除。面试里想答得精彩不要只说摇树去掉没用的代码要能讲清楚为什么必须是 ESM 而不是 CommonJS静态 vs 动态标记和删除是两个不同阶段、两个不同工具Webpack TersersideEffects是摇模块的关键不只是摇 exportBabel 配置不当是最常见的失效原因把这条链路讲通就从知道有 Tree Shaking升级到了理解 Tree Shaking 的完整机制。

更多文章