一次线上白屏排查:静态 import 是如何悄悄破坏 Webpack 共享 Chunk 的
背景
新功能上线后,陆续收到反馈:有用户打开某个页面时直接白屏,刷新后恢复正常。打开控制台一看,报了一个熟悉又陌生的错误:
ChunkLoadError: Loading chunk [xxx] failed.
(missing: https://.../static/js/async/[chunk-name].js)

文件找不到了。但奇怪的是,强制刷新后一切恢复正常。这说明问题不在于代码逻辑本身,而是浏览器缓存的旧资源与服务器上的新构建产物对不上了。
先搞清楚:Webpack 的共享 Chunk 是怎么回事?
现代前端项目几乎都会对路由做懒加载,每个页面只在用户访问时才加载对应的 JS。大致写法如下:
js
// router/index.ts
{ path: '/recharge', component: () => import('../views/Recharge/index.vue') }
{ path: '/balance', component: () => import('../views/MyBalance/index.vue') }
这样每个路由就对应一个独立的 async chunk,看起来互不干扰。
但 Webpack 不会止步于此。
Webpack 的 SplitChunksPlugin 会在构建阶段分析所有 async chunk 的依赖,一旦发现多个 chunk 都静态引入 了同一批模块,它就会把这些公共模块提取出来,合并成一个共享 async chunk,以避免重复加载。
这个共享 chunk 的文件名大概长这样:
src_components_Foo_index_vue-src_api_bar_ts-...-af6c4a.js
末尾的 af6c4a 是根据 chunk 内容 生成的 hash。只要 chunk 里包含的模块发生了任何变化,hash 就会变,文件名随之改变。
问题是怎么发生的?
本次迭代,我们在充值页面引入了一个新的支付工具模块。这个工具模块自身没有问题,但它在模块顶层 通过静态 import 引入了一个风控弹窗组件。
关键点来了:
- 迭代前:风控弹窗组件只被余额页静态引用,它只存在于余额页自己的 chunk 中。
- 迭代后:充值页通过支付工具模块,间接静态引用了同一个风控弹窗组件。
Webpack 重新分析依赖图后发现:「哦,这个风控弹窗现在是充值页和余额页的共同依赖了,我要把它提取到共享 chunk 里。」
于是共享 chunk 的模块组成发生了变化,旧 hash af6c4a 对应的文件在新构建中已不存在,取而代之的是一个新 hash 的文件。
触发崩溃的最后一步 :用户的浏览器或 CDN 缓存了旧版 HTML,HTML 里记录的还是旧 hash 的 chunk 文件名。浏览器按旧地址去请求,服务器上找不到,于是抛出 ChunkLoadError: missing,页面白屏。
为什么强刷就好了?
强制刷新(Cmd/Ctrl + Shift + R)会绕过缓存,重新从服务器拉取最新的 HTML。新 HTML 里记录的是新 hash 的 chunk 文件名,文件存在,一切正常。
这也印证了:代码逻辑本身没有 bug,问题出在旧缓存引用了已不存在的构建产物。
解决方案
短期:临时止血
通知受影响的用户强制刷新浏览器,或清除浏览器缓存,让其加载最新资源。
长期:从根源规避
问题的本质是:在公共工具模块的顶层写了一个静态 import,导致一个「条件性才会用到」的业务组件意外影响了 Webpack 的依赖图。
解法很直接------把静态 import 改成动态 import,让组件在真正需要的时候才加载:
js
// 改造前:模块顶层静态引入,影响依赖图
import RiskDialog from '@/components/RiskDialog'
// 改造后:在实际调用处动态引入,不污染依赖图
if (需要展示风控弹窗) {
const { default: RiskDialog } = await import('@/components/RiskDialog')
RiskDialog(...)
}
这样风控弹窗组件在构建阶段对其他模块完全「透明」,不会参与共享 chunk 的组成计算,hash 的稳定性得到了保障。
延伸思考:哪些组件应该用动态引入?
这次事故给了我们一个很好的筛选标准:
只在特定条件下才会触发的组件,不应该出现在模块顶层的静态
import中。
常见的例子包括:
- 风控 / 实名认证弹窗
- 协议确认弹窗
- 错误提示 / 异常兜底弹窗
- 各类重量级的业务弹窗
这类组件的特点是:用户不一定会触发它,但只要把它静态引入,Webpack 就必须把它算进依赖图 。改用动态 import 后,它们既不影响首屏加载体积,也不会干扰共享 chunk 的 hash 稳定性,一举两得。
总结
| 维度 | 说明 |
|---|---|
| 现象 | 页面白屏,控制台报 ChunkLoadError: missing |
| 根因 | 新迭代引入的静态依赖改变了共享 chunk 的模块组成,导致 hash 变化,旧缓存失效 |
| 触发条件 | 浏览器 / CDN 缓存了旧 HTML,仍引用旧 hash 的 chunk 文件 |
| 修复方向 | 将「条件性使用」的组件从静态 import 改为动态 import |
| 预防原则 | 公共工具模块中避免静态引入业务组件,尤其是非必然触发的弹窗类 |
Webpack 的共享 chunk 机制是一把双刃剑:它能自动优化加载性能,但也意味着任何一处静态依赖的变动都可能「牵一发而动全身」。理解它的工作原理,是写出对构建产物友好的代码的前提。