路由级代码分割踩坑:esModule与公共依赖的取舍

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-07 04:08 ·1 浏览 ·0 回复

上个迭代接手了一个后台项目,首屏白屏时间被要求压到 1.2 秒内。由于路由页面实在多,我第一时间想到路由级代码分割:`React.lazy + Suspense` 一把梭。改造后主包从 915KB 降到 270KB,正当我以为可以交差时,线上却接连冒出两个诡异问题:某个路由下公共库加载直接报错“Cannot read properties of undefined”;另一个更离谱,配了 `splitChunks` 的公共依赖竟然被打进了三份子包。排查到最后,原因都指向一个经常被忽略的选项——`esModule`,以及我们对公共依赖的“情结”出了偏差。

一切从路由懒加载开始

场景不复杂:一个后台管理前端,几十个路由页面,初始 `/login` 却要加载全量代码,明显不合理。于是常规操作:把页面组件改为 `React.lazy(() => import('./pages/Dashboard'))`,再用 Suspense 包一层。构建产物确实开始生成独立 chunk,路由切换时会自动拉取对应子包。

问题就出在后来的“优化”上。为了让子路由复用基础库,我把 `react`、`antd`、`axios` 等公共依赖全部塞进一个 `vendor` chunk,并在 `splitChunks` 里设了 `chunks: 'all'`。上线后,部分页面开始偶发报错,而且只发生在二次进入某条路由时。一开始怀疑是网络缓存问题,后来用 sourcemap 一跟,发现是公共包被多份打包时,模块初始化顺序发生了错乱。

esModule 选项带来的“惊喜”

真正查到根因,是在一个发行版里看到报错堆栈指向 `undefined.default`。Webpack 在拆分 chunk 时,会按照一种模块兼容协议去获取模块导出。如果配置了 `esModule: true`,它会尽可能用原生 ES Module 的方式引用其他 chunk 里的内容;而如果目标依赖本身是 CommonJS 写法,那么实际拿到的是一个 `module.exports` 对象,而非带 `default` 属性的导出。

我恰好把 `output.module` 打开了,又对 `splitChunks` 里某些公共依赖设置了 `esModule: true`,而第三方库内部是 CJS。结果就是,主包用 ESM 方式导入 vendor 的命名导出,vendor 又用 CJS 维护依赖,最终运行时出现了“拿不到 `default` 却硬要访问 `default`”的尴尬局面。这种错误在本地 Dev Server 中极难复现,因为热编译路径和最终产物形态不一致。

公共依赖的“复制粘贴”问题

第二个坑更隐蔽。明明把 `moment`、`lodash` 这类库写进了 `cacheGroup`,生成的子包却出现了多份副本。对照配置,感觉规则没问题,chunks 也设了 `all`。后来发现,问题还是出在“esModule 模式下的公共依赖归属”上。

当 `esModule: true` 时,Webpack 对异步 chunk 的依赖引用会生成一种更短小的运行时指令,但公共依赖要想跨多个异步 chunk 被复用,必须满足它在所有模块中都以完全一致的 ES Module 语义存在。一旦某个路由组件里通过 `import * as X` 或 `require(X)` 混着用,Webpack 会认为该依赖“无法在 ESM 协议下安全共享”,于是放弃提取,把它分别嵌套进各个 chunk。表面是你配了公共依赖拆分,实际每个 chunk 都私藏了一份隐私。

更麻烦的是,这种做法还会破坏文件指纹缓存。公共库版本没变,但因为某个路由组件改了代码,那个公共库的副本 hash 也变了,导致缓存失效,老用户要多下几十甚至上百 KB。

取舍与建议

路由级代码分割这件事,我觉得真正的核心不是“切得越碎越好”,而是“切完之后模块之间的共享关系是否还稳定”。针对 `esModule` 和公共依赖,我现在的取舍原则是:

- 如果项目输出目标还是传统浏览器,或者大量依赖仍是 CommonJS,老老实实保持 `esModule: false`。不要为了追赶潮流强行开 ESM,否则 CJS 与 ESM 互操作的成本会转移到运行时。
- 如果项目已经全面拥抱 ESM,第三方库版本和构建链都支持,那就将 `esModule: true` 和 `output.module: true` 一并使用,同时确保业务代码中不要混用 `require` 与 `import`。
- 公共依赖不要一股脑全塞进同一个 `vendor`。真正需要全局共享的只有 React、ReactDOM、路由核心这类几乎每个页面都用的基础框架;体积大但只在部分路由用到的图表、编辑器库,应该随各自路由 chunk 按需加载。

总结来说,代码分割不是简单地把路由改成懒加载就能终结。`esModule` 与公共依赖的分组,与其说是配置问题,不如说是对项目模块规范和浏览器目标的一次体检。只有先想清楚“模块之间用什么方式互相看见”,才能让每个异步 chunk 都安心干活。

全部回复 0

还没有回复,来抢沙发~