如何在CRA应用中对tsc编译的utils包进行Tree Shaking?
我维护两个npm包:
- 前端CRA应用(简称FE)
- 用于FE和其他包共享代码的utils包
FE依赖utils,但utils里包含仅被其他包使用、FE完全没用到的Node专属代码(无法在浏览器运行)。把FE的react-scripts从v4升级到v5(底层webpack从v4换成v5)后,CRA开发服务器打包的bundle里居然包含并执行了这些Node代码,直接导致浏览器崩溃。
我知道原因是utils通过index.ts导出所有代码,FE只从里面导入特定函数,但搞不懂为什么v4没问题v5就炸了。虽然可以拆分模块导入解决,但更希望通过Tree Shaking自动移除未使用的代码。FE用了CRACO,能修改webpack配置。
核心问题:
- 怎么配置Tree Shaking,让FE的bundle不再包含未使用的工具函数?
- 升级到v5后问题出现的原因是什么?
- 能不能复现v4的行为?
附utils包的配置信息:
utils/package.json 的scripts部分
"scripts": { "start": "yarn build", "build": "tsc", "watch": "tsc -w", "postinstall": "patch-package", "prepare": "yarn build", "test": "jest" }
utils/tsconfig.json
{ "compilerOptions": { "target": "es6" , "module": "commonjs" , "lib": ["es2015", "es2016", "es2017"], "downlevelIteration": true, "allowJs": false, "skipLibCheck": true, "declaration": true, "outDir": "dist", "strict": true, "noImplicitAny": true, "strictNullChecks": false, "noImplicitThis": false, "baseUrl": "../", "paths": { "@schema": ["schema/index"], "@schema/*": ["schema/*"] }, "esModuleInterop": true }, "include": ["types/**/*", "**/*.ts"], "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] }
一、升级到v5后出问题的根源
webpack v4和v5对CommonJS模块的处理逻辑差异是核心:
- webpack v4对CommonJS的Tree Shaking支持极弱,会默认打包整个模块,但CRA v4的默认配置可能通过其他机制(比如模块评估顺序、死代码消除的宽松规则),没触发那些未被引用的Node代码执行。
- webpack v5强化了CommonJS模块的Tree Shaking能力,但同时改变了模块评估方式——它会更精确地追踪导出对象的引用,可如果你的utils是CommonJS格式,webpack v5可能误判,把未被FE使用但存在于导出对象中的代码也打包进来,甚至执行模块顶层的Node API调用,直接导致浏览器崩溃。
另外,你的utils包用tsc编译时指定了"module": "commonjs",输出的是CommonJS模块,而Tree Shaking对ES模块(ESM)的支持远优于CommonJS,这也是关键诱因。
二、配置Tree Shaking的具体步骤
要让Tree Shaking真正生效,得从utils包的编译格式和FE的webpack配置两方面动手:
1. 改造utils包,输出ESM格式
Tree Shaking的前提是模块为ESM,先修改utils的tsconfig.json:
{ "compilerOptions": { // 将module改为ESNext或ES6,输出ESM格式 "module": "ESNext", // 新增模块解析配置,适配ESM "moduleResolution": "node", // 保留其他原有配置... } }
同时在utils的package.json中添加"type": "module",明确标记为ESM包:
{ "type": "module", // 其他原有配置... }
如果需要兼容CommonJS环境,可以用tsc编译双格式(ESM+CommonJS),并在package.json里指定不同入口:
{ "main": "./dist/cjs/index.js", // CommonJS入口 "module": "./dist/esm/index.js", // ESM入口,webpack会优先读取这个 "types": "./dist/types/index.d.ts" // 类型文件入口 }
对应的可以拆分出两个tsconfig文件(比如tsconfig.esm.json和tsconfig.cjs.json),分别编译到不同目录,在scripts里添加对应的编译命令即可。
2. 通过CRACO配置FE的webpack
在FE的craco.config.js中,确保Tree Shaking相关优化开启:
module.exports = { webpack: { configure: (config) => { // production模式下webpack默认已开启Tree Shaking,无需额外配置 // 开发环境默认不开启(为了热更新速度),如果想强制开启,添加以下配置(会影响构建速度) if (process.env.NODE_ENV === 'development') { config.optimization.usedExports = true; } // 如果暂时没法把utils改成ESM,可针对CommonJS模块开启更严格的Tree Shaking config.module.rules.push({ test: /\.js$/, include: /node_modules\/your-utils-package-name/, // 替换成你的utils包名 parser: { commonjs: { strictRequires: true } } }); return config; } } };
三、能不能复现v4的行为?
可以,但不推荐,因为这是治标不治本的临时方案,不利于长期优化。如果一定要临时解决,可在CRACO配置里让webpack v5对utils包采用类似v4的处理逻辑:
module.exports = { webpack: { configure: (config) => { // 关闭对utils包的严格CommonJS解析,回归v4的宽松处理 config.module.rules.push({ test: /\.js$/, include: /node_modules\/your-utils-package-name/, parser: { commonjs: { strictRequires: false } } }); // 或者用IgnorePlugin直接忽略Node专属文件 const webpack = require('webpack'); config.plugins.push( new webpack.IgnorePlugin({ resourceRegExp: /node-specific-file\.ts$/ // 替换成你的Node专属文件的正则 }) ); return config; } } };
但还是建议优先把utils改成ESM格式,才能真正实现可靠的Tree Shaking。
内容的提问来源于stack exchange,提问作者Michal Kurz

