静态站打开是白屏:3 个原因 + 修复路径

线上站打开 title 闪一下就剩白屏、HTML 在但 DOM 空——多半是 JS 抛错被吞、资源路径错或 CSP 拦了脚本。本文给一套 10 分钟定位根因的检查顺序。

打开线上 URL,浏览器标签页 title 闪一下就只剩空白:HTML 已经下载、DOM 已经挂上 <div id="root">,但里面什么都没渲染出来。这种”白屏不报错”几乎只有三类成因——客户端 JS 抛错被吞、资源路径错把 HTML 当 JS 加载、或 CSP 把脚本拦了。本文给出一套 10 分钟内能定位到根因的检查顺序,适用于 Astro、Next.js、Vite、CRA、Nuxt 等所有静态部署框架。

常见原因

按命中率从高到低:

1. 客户端 JS 在水合前抛错

最常见的一种:React/Vue 在第一次 render 就抛错,整棵树被丢弃,根节点变空。Console 里会有红色的 Uncaught TypeError: Cannot read properties of undefined (reading 'xxx')Hydration failed because the initial UI does not match

Uncaught Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418
  at chunk-XXX.js:1:12345

如何判断:DevTools → Console 看第一条红色错误。若是水合错误,多半是 server 渲染时间戳 / Math.random() / Date.now() 跟客户端不一致。

2. 资源 base path 错,JS 请求返回了 HTML

部署到子路径(/blog//docs/)或自定义域名时,build 时的 base 没改对,浏览器去请求 /assets/main.js 但服务器把 404 fallback 到 index.html,于是浏览器把整个 HTML 当 JS 解析,立刻报 Unexpected token '<'

Refused to execute script from 'https://example.com/assets/main-abc123.js' 
because its MIME type ('text/html') is not executable.

如何判断:DevTools → Network → 选中那个 JS 请求 → Response 标签页,如果看到 <!DOCTYPE html> 就是这个问题。

3. CSP 拦了内联脚本或外部 CDN

Vercel / Cloudflare / Netlify 上配了 Content-Security-Policy: script-src 'self',但你的框架注入了 inline <script>(Astro 的 is:inline、Next.js 的 __NEXT_DATA__),或者引用了 CDN(unpkg.comcdn.jsdelivr.net),全被浏览器拒绝执行。

Refused to execute inline script because it violates the following 
Content Security Policy directive: "script-src 'self'".

如何判断:Console 里搜 Content Security PolicyRefused to,每条都标注了被拒绝的 URL 和指令。

4. ES module 在老浏览器解析失败

build.target 设成了 esnext(默认输出 ES2022+ 语法),但用户浏览器是 Safari 14 之前 / 老 Edge / 微信内置浏览器,遇到 ??=#private 字段、top-level await 直接 SyntaxError,整个模块加载失败。

如何判断:在 BrowserStack 或同事的旧设备上打开,Console 报 SyntaxError: Unexpected token,行号指向 bundle 文件。

5. Service Worker 缓存了旧的破损版本

上一次部署出过半坏的版本,SW 把它缓存了下来,之后即使新部署修好了,老用户依然加载旧的破 JS,永远白屏。

如何判断:DevTools → Application → Service Workers,看到一个 active 的旧 worker;Application → Cache Storage 里有过期的 bundle 文件名。

最短修复路径

Step 1:用 DevTools 抓第一条错误

无痕窗口打开页面,DevTools 一定要在打开页面之前就打开(否则会漏掉早期错误)。然后:

1. 切到 Console,把所有红色行复制出来
2. 切到 Network,勾选 Disable cache,刷新一次
3. 按 Type 列排序,看 JS / Document 请求的 Status 和 MIME type

200 OK + text/html 出现在 JS 行 = 路径问题;200 OK + text/javascript 但 console 报错 = 代码问题;Status 0 / blocked = CSP 或网络拦截。

Step 2:按错误类别对应修复

错误信息真正原因修复方向
Unexpected token '<'base path 错vite.config / astro.config 里的 base
Hydration failedSSR/CSR 不一致<ClientOnly> 包住时间戳 / 随机数组件
Refused to execute inline scriptCSP 太严'unsafe-inline' 或 nonce
Cannot read properties of undefined数据为空加空值兜底 + error boundary
SyntaxError: Unexpected tokentarget 太新调低 build.targetes2018

Step 3:base path 修复

Vite / Astro 部署到 https://example.com/blog/

// astro.config.mjs
export default defineConfig({
  site: 'https://example.com',
  base: '/blog',
  trailingSlash: 'always',
});

Next.js:

// next.config.js
module.exports = {
  basePath: '/blog',
  assetPrefix: '/blog',
};

改完本地 npm run build && npm run preview 验证一次,再 push。

Step 4:根节点加 error boundary

让 JS 报错不再清空整页:

// React
import { ErrorBoundary } from 'react-error-boundary';

function Fallback({ error }: { error: Error }) {
  return (
    <div style={{ padding: 24 }}>
      <h1>出错了</h1>
      <pre>{error.message}</pre>
    </div>
  );
}

export default function App() {
  return (
    <ErrorBoundary FallbackComponent={Fallback}>
      <YourApp />
    </ErrorBoundary>
  );
}

Vue 用 errorCaptured,Svelte 用 <svelte:boundary>(5.0+)。

Step 5:清掉坏的 Service Worker

如果是老用户白屏新用户正常,几乎一定是 SW 缓存。临时方案 + 永久修复:

// public/unregister-sw.js  临时挂一份,让所有访客自动清缓存
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.getRegistrations().then(rs => {
    rs.forEach(r => r.unregister());
  });
  caches.keys().then(keys => keys.forEach(k => caches.delete(k)));
}

部署一周后再撤掉这个脚本。

预防建议

  • 根节点必加 error boundary,任何子组件崩了不会让整页变白
  • CI 里跑一次 Playwright/Puppeteer 冒烟测试,访问首页断言 document.body.innerText.length &gt; 100
  • 部署后用 WebPageTest 或 Lighthouse CI 跑一次,能发现 CSP / MIME 类型错
  • build.target 至少留两个 LTS 版本兼容性(如 Safari ≥ 14、Chrome ≥ 90)
  • Service Worker 注册时带版本号 + skipWaiting,避免老 SW 卡死新部署

相关阅读

标签: #部署 / 托管 #排查 #排查