tree-shaking未生效主因是模块格式与打包器分析能力不匹配:需确保esm规范、避免commonjs混用、精准导入子路径、检查sideeffects声明及导出方式,并在production构建后通过stats或可视化工具验证。

Tree-shaking 为什么没生效?先看打包器和模块格式
Tree-shaking 不是开关,而是依赖打包器(如 webpack、vite、esbuild)对 ESM(import/export)静态结构的分析能力。用 require 或 CommonJS 模块(比如 lodash 的 const _ = require('lodash'))基本无法摇掉任何东西——因为执行时才决定引用关系。
实操建议:
- 确保你的代码和所用库都以
ESM形式提供:查package.json中是否有"type": "module"或"exports"字段指向.mjs/ESM入口 - 避免混用:
import { debounce } from 'lodash'是 OK 的,但import _ from 'lodash'+_.debounce会把整个包引入 -
vite默认只对node_modules中明确标记sideEffects: false的包做深度摇;webpack需在optimization.usedExports: true下配合mode: 'production'
第三方库不支持 Tree-shaking?手动指定入口或别名
很多库(如 lodash、date-fns)虽支持 ESM,但默认 import 仍可能拉入未用代码,原因常是导出方式不够“纯”(比如有动态 key、export *、副作用初始化)。
实操建议:
- 优先用子路径导入:
import debounce from 'lodash/debounce',绕过主入口的聚合导出 - 对
date-fns这类已按功能拆包的库,直接import format from 'date-fns/format',比import { format } from 'date-fns'更安全 - 在
webpack中配resolve.alias把lodash映射到lodash-es;vite可用optimizeDeps.include+depPrebundle控制预构建行为
开发时看着没删,其实是 HMR 或 source map 干扰了判断
常见错觉:“我明明没用 fooUtils,但打包后体积没变”。往往不是 Tree-shaking 失败,而是你在 dev 模式下看产物(比如 webpack-dev-server 内存编译)、或启用了 devtool: 'source-map' 导致生成冗余映射信息。
实操建议:
- 只在
build后检查真实产物:npx webpack --mode=production --stats=verbose,关注chunks和provided exports字段 - 用
rollup-plugin-visualizer或source-map-explorer分析最终.js文件内容,确认某函数是否真被剔除 -
console.log、debugger、未删除的注释、/*#__PURE__*/标记缺失,都可能让打包器误判为“有副作用”,不敢删
你的代码里藏着“假死”导出,Tree-shaking 看不见
即使你写了 export const unused = () => {},只要它被任何地方 import 过(哪怕只是类型导入、测试文件、甚至被 IDE 自动补全加进去了),就不会被摇掉。更隐蔽的是:导出对象属性、默认导出解构、export * from —— 这些都可能让整块模块“活”下来。
实操建议:
- 检查所有
export *语句,尤其跨包聚合时;改用显式命名导出:export { foo } from './foo' - 避免默认导出对象:
export default { a, b }→ 摇不掉a就得留着整个对象;改用命名导出 - TypeScript 用户注意:
import type安全,但import { X } from 'x'即使只用于类型,也会触发实际导入(除非加isolatedModules: true并用import type)
真正卡住 Tree-shaking 的,往往不是配置没开,而是某处 export 被意外引用,或某个库的 sideEffects 声明不准确。盯着 stats 输出里的 usedExports 和 providedExports 对照看,比调参数更快定位问题。










