本文结合具体应用场景,说明我们遇到的在生产环境偶发整页空白的问题:在什么条件下出现、如何定位、项目内已采取哪些措施、以及仍依赖运维侧配合的部分。
1. 现象
- 用户侧:页面突然白屏或长时间无内容,常发生在发版后的一段时间内,且并非所有用户同时复现。
- 浏览器开发者工具中常见(可能同时出现多项):
Failed to load module script:期望加载 JavaScript 模块,响应的Content-Type却是text/html。Failed to fetch dynamically imported module/Importing a module script failed等动态import()相关错误。- 网络面板中:对
.../assets/*.js的请求返回 404,或返回 200 但正文为 HTML(实为 SPA 回退页)。
另有一条独立但常一起出现在控制台的问题:413 Request Entity Too Large(附件上传超过网关或服务端请求体限制)。它不直接导致「JS 白屏」,但会干扰排障,需在文档中区分。
2. 典型触发场景
2.1 主场景:长会话跨版本
- 用户用版本 A 打开站点,浏览器已加载当时的
index.html、主入口脚本及已访问过的 chunk。 - 用户未整页刷新,在单页应用内继续操作;期间线上发布版本 B,构建产物中 带内容哈希的
assets/*文件名变化,旧文件从服务器上被替换或删除。 - 用户点击新菜单等操作,触发此前未加载过的懒加载 chunk,浏览器仍按版本 A 打包时写入的 URL 去请求
assets/xxx-[旧hash].js。 - 该文件在版本 B 的部署目录中已不存在 → 请求失败;若网关对「未匹配到的静态路径」执行 SPA 回退(返回
index.html),则浏览器把 HTML 当 JS 执行 → 动态模块加载失败 → 白屏。
结论:该问题在**「发版瞬间仍有用户挂在旧版文档上」时更容易出现,因此表现为偶发**。
2.2 延伸场景:入口页被强缓存
即使用户没有在「发版过程中一直开着旧页」,若 index.html 被浏览器或 CDN 长期缓存,下次「打开站点」时仍可能拿到旧版入口,其中引用的仍是已不存在的旧 hash 资源,机理与上类似。是否出现取决于 HTTP 缓存策略,与是否「长会话」无关。
2.3 一般不易出问题的场景
- 发版后用户完整重新打开页面(新导航/硬刷新),且能拿到最新
index.html(入口未长期强缓存)时,入口中脚本 URL 与当前磁盘上的assets一致,通常不会因「旧 hash 被删」而白屏。
3. 根因归纳
| 层级 | 说明 |
|---|---|
| 构建与运行方式 | 现代打包(Vite 等)对 JS/CSS 使用内容哈希文件名;单页应用在首屏后多依赖 动态 import() 按路由懒加载 chunk。 |
| 版本不一致 | 同一用户在一次浏览中,执行中的主包(或已解析的 URL)与服务器当前实际存在的文件可能来自不同发版,导致请求旧 hash 文件失败。 |
| 网关/静态服务行为 | 对不存在的 /assets/* 使用与前端路由相同的 try_files → index.html 回退 时,对浏览器而言是「模块脚本 URL 返回了 HTML」,引发 MIME / 模块解析错误,与纯 404 相比更容易在控制台看到 text/html 类报错。 |
| 与 413 的关系 | 413 表示上传体积超过 Nginx client_max_body_size 或后端 body 限制,需调网关/服务配置;不是白屏主因,但会 concurrent 出现在控制台。 |
4. 排查要点
- 时间线:是否发生在发版后?是否未整页刷新仍复现、硬刷新后恢复?
- 控制台:是否存在上述 动态 import / module script 关键字;413 是否与上传同一时段出现。
- 网络:失败的请求 URL 是否为
/…/assets/*.js;状态码 404 还是 200 + HTML;响应头Content-Type是否为text/html。 - 区分:若仅为上传接口 413,优先按体积限制处理;白屏仍以 静态资源与入口一致性 为主轴。
5. 治理方案(分层)
5.1 HTTP 缓存与静态资源(根治方向,运维 / 网关)
目标:保证用户拿到的 index.html 始终足够新,同时允许对带 hash 且文件名唯一的资源长期缓存。
| 资源类型 | 建议 |
|---|---|
index.html | Cache-Control: no-cache 或极短 max-age,必要时 must-revalidate,避免长期强缓存旧入口。 |
/assets/ 下带 hash 的 JS/CSS | 可配置较长缓存(如 immutable);文件名变更即视为新资源,不会再去请求已删除的旧文件。 |
Nginx(子路径部署示例思路):
- 对
/scs/assets/(按实际前缀调整):仅映射到磁盘上真实文件,缺失则 404,不要对该前缀使用与 SPA 相同的「统一回退到index.html」,以免缺失 chunk 时仍返回 HTML。 - 对
/scs/api/等上传接口:按需增大client_max_body_size(并与后端限制一致),缓解 413。
具体片段需与现网 location、alias/root 一致,由运维在服务器配置中落地。
5.2 发布流程
- 原子切换:一次发布对外仅暴露一整套
index.html+assets/**,避免短时间内外混用两套不一致的产物(目录替换、软链切换等方式按团队规范执行)。
5.3 前端兜底(本项目已实现)
在无法立刻改变网关策略或仍存在边缘故障时,用前端降低「纯白屏」概率并给出可操作提示:
| 措施 | 文件 / 位置 | 行为简述 |
|---|---|---|
| 懒加载失败时一次自动刷新 | src/lib/lazyWithRetry.ts,路由见 src/router/index.tsx | 判定为疑似 chunk 加载错误且本会话未刷新过时 sessionStorage 标记后 location.reload() 一次,多数情况下可拉到新入口与新资源;避免无限刷新。 |
| 错误边界 | src/components/ChunkLoadErrorBoundary.tsx,src/App.tsx 包裹路由 | 仍失败时展示说明文案,引导强制刷新或清缓存,而不是长时间无反馈白屏。 |
| 上传 413 提示 | src/services/api.ts 响应拦截器 | 将 413 转为明确文案,避免用户误以为「页面坏了」。 |
说明:前端兜底不能替代「index.html 缓存策略 + assets 不误返回 HTML」;首屏主脚本若整体加载失败,React 仍未启动时,错误边界也可能无法覆盖,仍以 缓存与网关配置 为主。
6. 小结
- 场景:发版后旧会话继续操作、或
index.html被长期缓存仍指向旧 hash 资源时,请求已删除的assets/*.js→ 动态import失败 → 白屏;长会话跨版本是最直观的触发条件。 - 根因:入口与磁盘上的静态资源版本不一致;若网关对缺失资源返回 SPA 的 HTML,会表现为 MIME / 模块脚本类错误。
- 方案:优先 入口短缓存 + hash 资源长缓存、
/assets/缺失即 404 不回退 HTML、发布原子切换;前端lazyWithRetry+ChunkLoadErrorBoundary+ 413 提示 作为体验兜底;413 需在网关/服务端调整上传上限。