问题背景
把0.1秒就能复现的“白屏”丢给测试同学,他们会在群里疯狂艾特你;丢给老板,他只会问“多久能好”。Chat{GPT} 登录后页面空白就是这样一种“零报错、零提示、零线索”的三零故障——开发者本地一切正常,一到线上就只剩一块惨白的画布。
- 调试窗口没有 4xx/5xx,Network 面板甚至全是 200
- 路由明明跳转到
/chat,却连侧边栏都没渲染出来 - 刷新一次好了,再刷新又白屏,复现规律完全看运气
在迭代节奏按小时计算的今天,白屏每多存在 10 分钟,需求池就会多 3 个紧急插单。效率被拖垮的往往不是代码复杂度,而是“看不见的错误”带来的定位成本。
常见原因分析
把能触发白屏的环节画成一条“登录→认证→拉配置→渲染”流水线,80% 的故障都能归类到下面 5 个点:
认证令牌失效或作用域不足
典型场景:后端刷新access_token时把session_key写丢,前端拿到 401 却未触发重登。前端路由守卫死循环
异步鉴权结果返回前,路由守卫提前next()到空白页;或者next(false)写成next(),直接中断渲染。API 响应格式变更
例如/v1/me接口把user.name改成user.username,模板里直接{{ user.name }}导致整页 Vue 报错被errorCaptured吞掉,呈现就是白屏。骨架屏/微前端挂载失败
子应用打包后publicPath带版本号,Nginx 没配try_files,vendor.js404,主应用继续等待bootstrap生命周期,页面永远空白。Content-Security-Policy 拦截
开了严格 CSP 却忘记把'unsafe-inline'给script-src,导致内联脚本被拦,框架初始化直接失败。
技术解决方案
下面给出一条“先保活、再定位、最后根治”的三板斧流程,按顺序执行,基本能在 15 分钟内确认是前端还是后端的责任。
复现并保留现场
打开无痕窗口,用“Preserve log”+“Disable cache”抓一次完整请求链,导出 HAR 留档,防止后续刷新把错误冲掉。快速自检令牌
在 Console 里执行await (await fetch('/v1/me',{headers:{Authorization:Bearer ${localStorage.token}}})).json()
如果返回 401,直接跳到第 4 步;若能拿到用户信息,说明令牌有效,排除认证问题。确认框架是否启动
Vue 项目看__VUE_PROD_DEVTOOLS__是否存在;React 项目看root._internalRoot。未定义即说明框架源码都没跑起来,优先检查脚本 404 或 CSP。手动刷新令牌
调用/v1/refresh把refresh_token换成新access_token,写回localStorage后强制location.reload()。白屏消失则证明是静默刷新逻辑没跑通。打开框架级错误捕获
在入口文件最顶部加:window.addEventListener('error', e => console.trace('Global Error:', e)); window.addEventListener('unhandledrejection', e => console.trace('Unhandled Promise:', e));重新构建上线,白屏时马上看日志,通常能抓到模板渲染异常或路由死循环的栈。
本地代理比对
用 Whistle/Charles 把线上域名映射到本地打包目录,若本地正常而线上白屏,99% 是资源路径或 CDN 跨域问题。
代码示例
以下示例基于 Vue3 + Vue-Router 4,其他框架思路一致,可直接迁移。
路由守卫:保证异步鉴权完成后再放行
// router/index.js import { getUserInfo } from '@/api/user'; router.beforeEach(async (to, from, next) => { if (to.meta?.requiresAuth) { try { // 优先读缓存,没有再请求 if (!store.user) await getUserInfo(); next(); // 鉴权成功 } catch (e) { next('/login'); // 失败就回登录页,避免空白 } } else { next(); // 公开页面直接放行 } });令牌自动刷新:在 axios 响应拦截里做静默刷新
// utils/request.js let isRefreshing = false; // 防止并发刷新 axios.interceptors.response.use( res => res, async err => { const { config, response } = err; if (response?.status === 401 && !config._retry) { config._retry = true; if (!isRefreshing) { isRefreshing = true; await refreshToken(); // 调用刷新接口 isRefreshing = false; } return axios(config); // 重试原请求 } return Promise.reject(err); } );全局错误兜底:把白屏转成可读的报错页
// main.js import { createApp } from 'vue'; import ErrorComp from '@/components/Error.vue'; const app = createApp(App); app.config.errorHandler = (err, vm, info) => { console.error('[Render Error]', err, info); vm.$root.$el.replaceWith(ErrorComp.$el); // 把整页替换成错误提示 };
性能与安全考量
- 令牌存储:禁止放 Cookie 的
HttpOnly=false里,避免 XSS 直接盗取;推荐内存 + 刷新令牌双轨制,内存变量页面关闭即失效。 - 刷新风暴:并发 401 时,必须加
isRefreshing锁,否则 N 个请求会同时换 N 次令牌,后端极易被误判为“刷接口”。 - 脚本加签:若用 CDN,开启 SRI(Subresource Integrity)并在 CSP 加
require-sri-for script,防止劫持注入。 - 白屏重试次数:对静默刷新失败设置上限(建议 2 次),超过后强制跳转登录页,避免无限循环请求把流量打满。
避坑指南
- 本地代理正常 ≠ 线上正常,一定在“禁用缓存 + 慢 3G”模式再测一次,很多 404 是 CDN 边缘节点延迟同步导致的。
- 骨架屏图片请用
base64内嵌,不要走外链;一旦 CSP 拦了img-src里的self,首屏就白给你看。 - 微前端场景下,主应用和子应用使用同一套
key管理令牌,刷新接口必须幂等,避免子应用刷新后主应用令牌失效。 - 日志上报要“先落地、后上报”,如果白屏导致框架未初始化,依赖框架的上报 SDK 也发不出去,用 `
兜底。 5. 灰度发布时,把新旧版本放在不同子目录,用try_files $uri $uri/ /dist-xxx/index.html` 兜底,防止路径 404 引发白屏。
互动环节
你在集成 ChatGPT 或其他大模型网页时,还遇到过哪些“无声”的白屏场景?
欢迎在评论区贴出报错截图或请求链,我会挑典型问题更新到正文,并附上后续排查录屏。一起把白屏耗时从小时级压到分钟级。
把排查流程跑通后,我发现很多“灵异白屏”其实都能复用到其他实时交互场景——比如最近我在从0打造个人豆包实时通话AI动手实验里,同样用到了“令牌保活 + 全局错误兜底”思路,让语音链路在弱网环境下也能秒级重连,全程没再出现“说了话没反应”的空白尴尬。实验把 ASR→LLM→TTS 整条链路拆成可插拔模块,跟着跑一遍,你会对“实时交互白屏”有更深的体感。