window.onerror 没有上报,不等于用户看到了正常页面。脚本可以不报错,但接口一直挂起、关键组件没有挂载,或者 WebView 只剩一张空背景;反过来,页面也可能抛出一个非关键异常,但主要内容仍然可见、可操作。
因此,白屏不是某一种 JavaScript 异常,而是“用户在一段时间内没有看到有效内容”的可用性问题。本文把视觉空白、关键元素、错误事件和时间阈值合并成一个确定性分类器,并用 7 项本地测试验证 confirmed、suspected、recovered、excluded、healthy 五种结果。
一、错误事件和白屏不是同一个集合
浏览器错误信号、性能信号和视觉结果回答的是不同问题。把任何一个信号当成白屏真相,都会制造盲区。
| 信号 | 能回答什么 | 单独使用的盲区 |
|---|---|---|
error | 同步脚本错误、部分资源加载失败 | 没报错也可能没有有效内容 |
unhandledrejection | 未处理的 Promise 拒绝 | 不代表主页面一定不可用 |
PerformanceObserver | 性能时间线中的指定事件 | 有绘制不等于业务内容可用 |
| 关键元素 | 业务主体是否已经出现 | 选择器失效会造成误判 |
| 视觉空白 | 用户画面是否长期接近空白 | 大面积留白、骨架屏可能误报 |
MDN 对 error 的说明明确区分了同步脚本错误与未处理 Promise:后者会触发 unhandledrejection。这已经说明“只接一个 window.onerror”连异常面都覆盖不全,更不用说没有异常但内容始终未出现的白屏。
二、把布尔告警改成五种结果
生产监控最好不要只输出 isBlank: true/false。同样是“超过阈值仍空白”,是否存在导航失败、是否最终恢复、是否属于已知过渡页,处理方式完全不同。
| 状态 | 最小判定 | 推荐动作 |
|---|---|---|
confirmed | 视觉空白超阈值、关键元素未出现,并有终止性错误 | 立即告警,关联版本与错误证据 |
suspected | 只有单一异常信号,或证据尚不完整 | 采样保留,等待更多信号或人工复核 |
recovered | 曾经超阈值空白,但关键元素随后出现 | 统计恢复耗时,排查慢启动 |
excluded | 命中已知过渡页、测试页或白底设计页 | 不计入主指标,但记录排除规则版本 |
healthy | 关键元素在阈值内出现,且没有终止性错误 | 进入正常性能统计 |
这里的五类是本文的工程建模,不是浏览器标准。它的价值在于让告警、发布门禁和排障动作对应到可解释证据,而不是把所有异常压成一个数字。
三、视觉检测解决“代码根本没跑”的盲区
经过脱敏整理的一份历史 WebView APM 资料采用过端侧截图分析:容器启动后周期采样画面,根据像素纯度判断有效内容是否出现。历史方案记录了 100ms 采样、Android 95%、iOS 98%、超过 5 秒判白屏、最长 10 秒等参数。
这些数字只属于当时的设备、页面和容器条件,不能直接复制到新项目。新系统至少要重新验证深色模式、骨架屏、大面积留白页面、Canvas/视频、采样开销和隐私边界。端侧最好只计算特征并立即释放位图,不保存或上传完整截图。
四、浏览器侧要收集互补信号
浏览器侧可以把同步错误、资源失败和未处理 Promise 分开采集,再由业务代码明确标记关键内容是否就绪。资源错误通常需要在捕获阶段监听,避免只盯着 window.onerror 的五参数回调。
window.addEventListener("error", (event) => {
const target = event.target
const isResource = target instanceof HTMLScriptElement
|| target instanceof HTMLLinkElement
|| target instanceof HTMLImageElement
reportSignal(isResource ? "resource_error" : "script_error")
}, true)
window.addEventListener("unhandledrejection", () => {
reportSignal("unhandled_rejection")
})
PerformanceObserver 可以观测受支持的性能条目,但它是性能时间线入口,不是白屏裁判。生产代码应先检查 PerformanceObserver.supportedEntryTypes,并把“首次绘制”“关键元素出现”“业务可交互”保留为不同时间点。
五、先统一事件,再做确定性分类
各端信号最终应归一成一个不含截图和敏感信息的观察对象。算法版本、阈值和排除规则必须进入事件,否则阈值调整前后的白屏率无法直接比较。
const observation = {
route: "/checkout",
visualBlankMs: 6200,
thresholdMs: 5000,
keyElementSeen: false,
terminalError: true,
excluded: false,
algorithmVersion: "visual-v3+signals-v1",
}
terminalError 不应等于“出现任意 JS 错误”。它应由导航失败、主文档 HTTP 失败、关键资源不可恢复或 WebView 进程异常等明确条件归并;非关键埋点报错最多提供疑似证据。
六、分类器的关键是拒绝单信号定罪
本地实验使用一个纯函数固定判定顺序:先处理排除项,再识别恢复,随后才确认白屏;只要证据不完整,就降级为 suspected。
export function classifyObservation(observation) {
if (observation.excluded) return { state: "excluded" }
const crossed = observation.visualBlankMs > observation.thresholdMs
if (crossed && observation.keyElementSeen) return { state: "recovered" }
if (crossed && !observation.keyElementSeen && observation.terminalError) {
return { state: "confirmed" }
}
if (crossed || !observation.keyElementSeen || observation.terminalError) {
return { state: "suspected" }
}
return { state: "healthy" }
}
阈值比较使用严格大于,因为历史文字口径是“超过 5 秒”。这类边界必须写进测试,避免不同端产生不一致统计。
七、7 项测试实际验证了什么
实验运行在 Node.js v22.19.0,使用内置 node:test,没有安装第三方依赖。7 项测试覆盖长期空白加终止错误、只有长期空白、超时后恢复、已知排除页、快速可见、页面可见但脚本报错,以及阈值相等的边界。
tests 7
pass 7
fail 0
{"confirmed":1,"suspected":2,"recovered":1,"excluded":1,"healthy":1}
这些结果只证明示例规则与测试一致,不证明真实像素算法有效,也没有测量 WebView 截图开销、误报率或线上白屏下降幅度。
八、指标要能推动发布决策
有了可解释状态,可以继续统计确认白屏率、超阈值恢复率、疑似事件确认率、关键元素就绪时间 P50/P75/P95,并按 App、WebView、系统、网络、页面和资源包版本分组。
发布系统可以先在灰度组比较新旧版本:若确认白屏率显著上升,或某个离线包版本集中触发 confirmed,暂停全量并回滚。阈值本身不能凭感觉设置,应来自页面样本、用户等待预算和设备性能分层。
九、上线前检查表与验证边界
- 同时采集视觉、关键元素、导航/HTTP、资源、脚本和 Promise 信号;
- 任意单一错误不会直接升级为确认白屏;
- 五种分类结果和阈值边界都有测试;
- 事件包含算法版本、页面版本和资源包版本;
- 端侧不上传完整截图,不记录用户敏感内容;
- 发布门禁使用确认白屏率和样本量,不被单个偶发事件触发;
- 每次调整采样区、阈值或分类规则都重新标定。
来源:
验证边界 本文核验了 MDN 对错误事件、未处理 Promise 与 PerformanceObserver 的说明,并完成零依赖分类实验;历史截图参数来自脱敏资料,只作为方法来源和测试样例。本文没有采集真实用户画面,没有验证阈值、像素算法、WebView 开销、误报率或线上改善幅度,新项目必须重新标定。