【Web】Next.js 静态站点部署后用户看到满屏 txt 代码?一文彻底解决
Next.js 静态站点部署后用户看到满屏代码?一文彻底解决
适用场景:Next.js
output: "export"静态导出 + Cloudflare Pages(或任何静态托管平台)
一、问题现象
部署新版本后,部分用户在页面上点击导航时,整个页面突然变成了一堆看不懂的代码文本——既不是报错页面,也不是白屏,而是原始的 HTML/JS 源码直接渲染在屏幕上。

典型触发路径:
- 用户打开页面正常使用
- 页面在浏览器标签页中静置一段时间(用户去做其他事)
- 期间开发者部署了新版本
- 用户回来继续点击操作
- 页面显示乱码般的代码文本
二、根本原因
2.1 Next.js 静态导出的文件结构
Next.js 在构建时会生成带有 内容哈希 的 JS chunk 文件:
out/
├── index.html
├── _next/
│ └── static/
│ ├── chunks/
│ │ ├── pages/dashboard-a1b2c3.js ← 哈希值 a1b2c3
│ │ └── pages/settings-d4e5f6.js
│ └── abc123/
│ └── _buildManifest.js ← 构建标识 abc123
HTML 文件中硬编码了这些带哈希的文件路径:
<script src="/_next/static/chunks/pages/dashboard-a1b2c3.js"></script>
2.2 版本不匹配的灾难链
┌──────────────┐ 部署新版本 ┌──────────────┐
│ 旧版 HTML │ ──────────────→ │ 新版文件系统 │
│ 引用 a1b2c3 │ │ 只有 x7y8z9 │
└──────┬───────┘ └──────┬───────┘
│ │
│ 用户点击导航 │
│ 请求 dashboard-a1b2c3.js │
│ │
▼ ▼
┌────────────────────────────────────────┐
│ 文件不存在 → 返回 404 HTML 页面 │
│ 浏览器把 HTML 当 JS 解析 → 解析失败 │
│ 页面直接渲染出原始代码文本 💥 │
└────────────────────────────────────────┘
一句话总结:旧 HTML 中的 JS 引用指向了已被新版本替换掉的文件,请求 404 后平台返回的错误页被浏览器当作代码渲染。
三、解决方案
我们采用 两层防护 策略,确保用户在任何情况下都不会看到乱码。
3.1 第一层:资源加载失败自动刷新
在 layout.tsx 的 <head> 中注入一段内联脚本,在页面最早期就开始监听资源加载错误:
// src/app/layout.tsx
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function(){
var reloaded = sessionStorage.getItem('__chunk_reload');
if (reloaded === location.href) {
sessionStorage.removeItem('__chunk_reload');
return;
}
window.addEventListener('error', function(e) {
var t = e.target;
if (t && (t.tagName === 'SCRIPT' || t.tagName === 'LINK')) {
var src = t.src || t.href || '';
if (src.indexOf('/_next/') !== -1) {
sessionStorage.setItem('__chunk_reload', location.href);
location.reload();
}
}
}, true);
})();
`,
}}
/>
</head>
工作原理:
| 要点 | 说明 |
|---|---|
| 捕获模式监听 | 使用 addEventListener('error', fn, true) 捕获阶段,在错误冒泡前就能拦截 |
| 精准匹配 | 只处理 <script> 和 <link> 标签,且 URL 包含 /_next/ 的资源 |
| 防死循环 | 通过 sessionStorage 标记当前 URL,刷新一次后如果仍然失败则不再刷新 |
| 用户无感 | 对用户而言只是页面闪了一下就恢复正常 |
3.2 第二层:Error Boundary 兜底页面
创建 src/app/error.tsx,当 Next.js 运行时捕获到 chunk 加载异常时展示友好提示:
// src/app/error.tsx
"use client";
import { useEffect } from "react";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
const isChunkError =
error.message.includes("ChunkLoadError") ||
error.message.includes("Loading chunk") ||
error.message.includes("Failed to fetch dynamically imported module") ||
error.message.includes("Importing a module script failed");
if (isChunkError) {
const key = "__chunk_reload";
if (sessionStorage.getItem(key) !== location.href) {
sessionStorage.setItem(key, location.href);
location.reload();
return;
}
sessionStorage.removeItem(key);
}
}, [error]);
return (
<div className="flex min-h-screen items-center justify-center ...">
<div className="text-center space-y-5">
<div className="text-6xl animate-bounce">🛸</div>
<h2>哎呀,页面走丢了~</h2>
<p>我们刚刚偷偷升级了系统 ✨ 刷新一下就能看到全新的页面啦!</p>
<button onClick={() => location.reload()}>刷新试试 🔄</button>
<button onClick={reset}>再来一次</button>
</div>
</div>
);
}
这一层能捕获的异常类型:
ChunkLoadError— Webpack chunk 加载失败Loading chunk xxx failed— 动态 import 的 chunk 404Failed to fetch dynamically imported module— ESM 动态导入失败Importing a module script failed— 浏览器原生模块加载失败
四、Next.js 配置优化
在 next.config.ts 中,以下配置与该问题相关:
// next.config.ts
const nextConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
// ---- 以下配置与部署稳定性相关 ----
trailingSlash: true,
skipTrailingSlashRedirect: true,
onDemandEntries: {
maxInactiveAge: 60 * 60 * 1000,
},
experimental: {
optimizeCss: false,
staleTimes: {
dynamic: 0,
},
},
};
各配置项作用
| 配置项 | 值 | 作用 |
|---|---|---|
trailingSlash | true | 为所有路由生成 /path/index.html 而非 /path.html,确保 Cloudflare Pages 的目录式路由能正确匹配,避免路径解析歧义导致的 404 |
skipTrailingSlashRedirect | true | 跳过 Next.js 默认的尾斜杠重定向逻辑,静态导出模式下无服务端可执行重定向,开启此项避免不必要的 redirect 配置冲突 |
onDemandEntries.maxInactiveAge | 3600000(1小时) | 开发模式下页面保留时间,设置为 1 小时可减少开发时的频繁重新编译,提升开发体验 |
experimental.optimizeCss | false | 关闭实验性的 CSS 优化,避免静态导出时 CSS 内联或压缩引发的兼容性问题 |
experimental.staleTimes.dynamic | 0 | 将客户端路由缓存的过期时间设为 0,确保每次导航都获取最新页面数据,不使用过期的缓存版本 |
其中
staleTimes.dynamic: 0对解决本文问题尤为关键——它避免了客户端路由器使用过期的缓存 prefetch 数据,减少版本不匹配的窗口期。
五、防护机制流程图
用户点击导航
│
▼
Next.js 请求 JS chunk
│
├── ✅ 成功 → 正常渲染页面
│
└── ❌ 失败(404)
│
▼
第一层:内联脚本捕获
<script>/<link> error 事件
│
├── 检测到 /_next/ 资源 404
│ ├── 首次失败 → sessionStorage 标记 → 自动 reload
│ └── 已刷新过 → 放行,交给下一层
│
▼
第二层:Error Boundary
捕获 ChunkLoadError 等异常
│
├── 首次失败 → 自动 reload
└── 已刷新过 → 展示友好错误页面
「哎呀,页面走丢了~」
[刷新试试 🔄] [再来一次]
六、总结
| 层级 | 文件 | 捕获时机 | 处理方式 |
|---|---|---|---|
| 第一层 | layout.tsx 内联 <script> | 资源标签加载失败(最早) | 静默自动刷新 |
| 第二层 | error.tsx Error Boundary | React 运行时异常 | 自动刷新 + 友好提示页 |
| 配置层 | next.config.ts | 构建阶段 | 减少缓存、规范路由,缩小问题窗口期 |
三层配合,让用户在版本更新时的体验从「满屏乱码,一脸懵逼」变成「页面闪了一下,继续使用」。
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 时光·李记
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果


