为什么我的Webpack配置了Tree Shaking还是没摇掉未使用的代码?
我在项目里按教程配置了Webpack的Tree Shaking,把mode设成production,也设置了optimization.usedExports为true,但打包后的bundle里还是能看到没用的lodash函数。比如在某个组件里只用了_.get,但打包结果里包含了整个lodash。我甚至在package.json加了”sideEffects”: false也没用,这是哪里出问题了?
尝试过用terserPlugin压缩,但控制台警告说某些代码无法删除。检查代码时发现有些第三方库是通过CommonJS引入的,比如:
const _ = require('lodash');
const get = _.get;
是不是CommonJS语法导致Tree Shaking失效?或者需要额外配置babel?
另外,如果依赖的库没有提供ES模块版本,该怎么处理才能让Tree Shaking生效?
更具体点说,当你这样写:
Webpack看到的是“我引入了一个叫 lodash 的包”,但它不知道你只用了 get 方法。整个 lodash 被当作一个整体导入,即使你只用了一个方法,打包时也会把整个库包含进去——这不是Webpack的问题,而是模块系统的限制。
解决办法分三步走:
第一步:改用ESM语法 + 按需引入
不要直接引入整个lodash,换成 import { get } from 'lodash-es'。注意这里要用 lodash-es 而不是 lodash,因为官方lodash主版本虽然提供了ESM,但在某些构建环境下仍然会回退到CommonJS行为。lodash-es 是专门为ES构建设计的版本,确保输出的是真正的ES模块。
第二步:确认你的babel配置不会把ESM转成CommonJS
很多人踩坑在这里。如果你用了Babel,并且.babelrc或者babel.config.js里有@babel/preset-env,默认情况下它会把ES模块转换成CommonJS,除非你明确告诉它不要。
检查你的babel配置:
这个modules: false的作用就是保留import/export语法原样不动,交给Webpack去做模块处理。如果没关掉,你代码里写的import最后变成require,Tree Shaking直接失效。
第三步:确保第三方库支持sideEffects标注并且正确使用
你在package.json加了"sideEffects": false是对的,但这表示整个项目没有副作用,适用于你自己写的代码。对于node_modules里的库,它们自己得声明sideEffects。比如一些组件库或工具函数库会在自己的package.json里写:
这样Webpack就知道除了这些文件外其他都可以摇。但如果库本身没提供ESM版本或者没有正确标注,你就没法摇。
那如果依赖的库压根不提供ES模块版本怎么办?
有两个方案:
方案一:用webpack的module.rules手动把某些CommonJS转成ESM
不太现实,因为这需要重写AST,复杂度高,容易出错。
方案二:推荐做法——换库或使用插件
例如针对lodash,你可以直接上 lodash-webpack-plugin 配合 babel-plugin-lodash 做自动按需引入。
先装两个包:
然后改babel配置:
再在webpack.config.js里加上插件:
这样就算你还用const _ = require('lodash'),babel会在编译时把它重写成只引入你需要的方法,相当于自动做了import { get } from 'lodash/get'这种事。
最后提醒一点:Terser压缩阶段报“某些代码无法删除”的警告,通常是因为检测到了可能有副作用的语句,比如全局变量赋值、原型链修改等。Tree Shaking不只是删代码,还要保证程序行为不变。所以哪怕标记了unused,如果有潜在副作用,Terser也不敢动。
总结一下:
1. Tree Shaking依赖ESM + 静态分析,CommonJS天然不支持
2. 必须用import/export语法,且babel不能转成CommonJS(modules: false)
3. 第三方库要提供ESM版本,最好配合sideEffects声明
4. 对于不支持的库,优先考虑替换或使用专用插件(如lodash系列方案)
你现在回去查一下打包后的bundle,大概率发现lodash还是被完整引入,就是因为require触发了CommonJS路径。改成import { get } from 'lodash-es'之后,再看chunk大小,应该能明显变小。
const _ = require('lodash'),这就是问题所在——CommonJS的导入方式会让Webpack无法静态分析模块依赖,Tree Shaking直接失效。因为require是运行时加载,Webpack在打包阶段根本没法确定你到底用了哪些方法。通用的做法是改用ES6的
import语法:注意要用
lodash-es而不是lodash,因为原版lodash发布的是编译后的CommonJS模块,而lodash-es提供了ES模块格式,才能被静态分析,从而支持Tree Shaking。如果你坚持用
lodash,至少得这样按需引入:这种方式也能摇掉没用的代码,因为Webpack能明确知道你只用了哪个模块。
另外你说在package.json加了
"sideEffects": false没用,那是因为你的引入方式本身已经让整个模块被标记为“可能有副作用”了。只有当项目依赖的库本身是ESM且没有副作用时,这个配置才起作用。总结一下关键点:
1. 别用
require引入第三方库,改用import2. 优先选提供ESM的库,比如
lodash-es、ramda这些天然支持Tree Shaking3. 如果库不支持ESM,就手动按需引入路径,比如
import debounce from 'lodash/debounce'4. 确保babel不要把ESM转成CommonJS,检查
.babelrc或babel.config.js里有没有@babel/plugin-transform-modules-commonjs,有的话在Webpack环境下得关掉最后,Webpack的Tree Shaking不是魔法,它依赖静态可分析的模块结构。只要有一环是CommonJS,整条链就断了。所以看到bundle里还是有整个lodash,基本可以断定是你用了
require('lodash')或者 babel 配置把 import 给转没了。