一、背景与意义
1.1 白屏问题的普遍性与严重性
白屏,指的是用户在打开网页时,页面长时间处于空白状态,未渲染出任何可见内容。随着前端技术的发展,这种问题并未减少,反而在复杂业务与重依赖架构中愈发常见,主要体现在以下几个方面:
-
首屏依赖重,渲染路径长
现代前端应用往往基于 Vue、React 等框架构建,页面渲染依赖多个异步资源(HTML、JS、CSS、字体、接口数据等),其中任何一步失败或延迟都可能导致白屏。
-
打包体积大,加载成本高
应用规模增长后,首屏资源动辄数 MB,受限于网络环境(特别是弱网/海外),资源加载慢或失败引发页面无响应。
-
第三方依赖引入白屏隐患
广告脚本、监控 SDK、CDN 服务等第三方资源如果超时或异常,会直接影响主流程渲染,甚至导致挂载失败。
-
WebView 容器表现不一
在 Hybrid 应用中,WebView 渲染行为受系统、设备影响较大,低版本 Android/iOS 容器中白屏问题更易发生,且难以复现与排查。
-
传统监控覆盖盲区
白屏常常不是"程序报错"导致,而是"未渲染成功",这类问题难以被传统 JS 错误监控(如 window.onerror)捕获,形成体验黑洞。
白屏问题具有高频率、高隐蔽性、难定位、影响大的典型特征,是当前前端体验稳定性治理中的一类重点问题。
1.2 白屏对用户体验和业务的影响
白屏问题的直接后果是用户"看到一个打不开的页面",对用户体验和业务造成以下多重影响:
- 跳出率提升:用户往往在 3 秒内判断页面是否正常,若页面白屏超过 3~5 秒,极易被认为"打不开";
- 转化率下降:尤其对电商、社交、活动页、登录页等首屏要求高的页面,白屏直接影响转化漏斗;
- 用户信任感丧失:白屏会导致用户对产品质量、品牌形象产生负面认知;
- 客服与运营成本增加:当白屏成为用户投诉的高发问题时,会增加支持团队的工作量;
- 技术定位困难:白屏本质可能并非"错误",而是渲染未完成或逻辑问题,排查成本高、恢复慢。
1.3 白屏检测的必要性
在上述背景下,建立一套系统性的白屏检测与监控机制显得尤为必要,其意义主要体现在:
- ✅ 感知体验问题:从用户视角发现"虽然没报错但体验很差"的问题;
- ✅ 快速定位原因:结合性能指标、错误日志、DOM 状态等数据进行还原与排查;
- ✅ 监控白屏趋势:帮助产品与技术团队评估不同版本、不同页面的白屏率变化;
- ✅ 度量体验质量:作为体验质量治理体系的重要指标之一,支撑版本发布、灰度策略;
- ✅ 辅助运维告警:当白屏率短时间内升高时,可触发预警机制,及时响应。
因此,白屏检测不仅是前端可观测性的必要组成部分,也是提升整体用户体验和产品稳定性的有效手段。
二、白屏的定义与表现
2.1 白屏的典型表现
"白屏"是指用户打开网页后,在预期时间内(如 1~5 秒)页面无法渲染出任何实际可见的业务内容,只看到背景色或纯白页面,给用户造成"页面加载失败"或"网站打不开"的主观体验。
白屏的典型表现包括但不限于以下几种:
- 页面整个区域为白色或背景底色,无任何文字或结构内容;
- 页面加载完成后仍然空白,无导航栏、主内容、按钮等;
- 页面 title 正常显示,但页面区域无内容;
- 用户点击链接进入新页面后,页面卡在空白状态,需手动刷新;
- 页面结构可能已存在于 DOM 中,但样式未加载或全部透明,用户看不到;
- 页面实际已渲染,但被一个全屏遮罩层(如 loading)误遮挡;
- 网页显示404,502等其他状态码,常见如SSR渲染;
💡 举例说明:
-
首屏加载资源失败
HTML 加载成功,但主 JS 文件因 CDN 故障加载失败,React 应用无法挂载,页面空白。
-
Vue 应用逻辑中断
接口鉴权失败,导致未进入
router.beforeEach
的回调,主页面未渲染,页面为空。 -
WebView 特定版本兼容问题
某版本 iOS WKWebView 渲染失败,页面明明加载完成但屏幕空白(崩溃或者闪退)。
-
懒加载模块未触发
首页使用异步组件按需加载,但逻辑判断错误,组件未被实际加载渲染。
-
被全屏 loading 覆盖
v-if="isLoading"
控制的全屏 loading 层未正确置为 false,导致主页面内容永远被遮住。
2.2 白屏与骨架屏、加载状态的区别
白屏并不等于页面"尚未加载完成"。在良好体验设计中,常见的还有"骨架屏"和"loading 状态",它们虽也未展示最终内容,但不会给用户"页面坏了"的感受。
类型 | 是否可视 | 用户感知 | 页面状态 | 是否可接受 |
---|---|---|---|---|
❌ 白屏 | 否 | 页面打不开、出错 | 无 DOM 或 DOM 不可见 | 否 |
✅ 骨架屏 | 是 | 页面正在加载中 | 结构占位已渲染 | 是 ✅ |
✅ loading 动画 | 是 | 页面正在初始化中 | 通常为动画遮罩层 | 是 ✅ |
⚠️ 伪白屏 | 是 | 内容加载中但看不到 | 内容被遮挡或透明 | 否(体验差) |
🤔 业务白屏 | 是 | 页面有内容 | 内容渲染异常不符合预期 | 否(体验差) |
🆚 差异解析:
- 白屏:页面真正没有任何可见的结构内容,或者有但用户看不到(例如样式未加载、透明、被遮挡),属于严重问题。
- 骨架屏:一种友好的过渡设计,通过骨架结构占位减少用户等待焦虑,告诉用户"页面正在努力加载"。
- Loading 状态:通常表现为转圈动画、进度条等,表示页面还在执行初始化逻辑,但不会空白。
- 伪白屏:表面上是"白",但其实页面结构在,只是被遮住或逻辑未更新,属于"误导型白屏"。
- 业务白屏:页面展示通用的报错兜底界面或者如404,502 这种页面"。
💡 举例说明:
-
骨架屏正常示例
京东、淘宝等电商首页加载时显示骨架图 ------ 左侧商品图、右侧文字块模拟,2 秒后替换为真实内容。
-
伪白屏示例
页面加载成功但样式加载失败,所有元素在但
display: none
;或全屏 loading 元素未隐藏,导致主内容被永久遮盖。 -
Loading 正常示例
微信小程序加载页面时转圈,同时底部显示"加载中,请稍候..."。
-
业务白屏示例
页面渲染异常,如页面展示404,502 等状态码,或者接口鉴权失败等,服务端数据返回有问题,页面展示"出错了"等兜底内容。
✅ 小结
- 真正的白屏,是页面内容从用户角度看"不存在",业务白屏用户能看到,但是不符合预期,影响用户体验;
- 骨架屏与 loading 动画是"可控"的等待体验;
- 白屏需要技术手段监测和修复,骨架和 loading 是设计手段预防"白屏感";
- 有效地区分这些状态有助于提升体验监控精度,避免误判。
三、出现白屏的原因
3.1 资源加载失败(404)
资源加载失败是最常见的白屏诱因之一,尤其是在首屏强依赖大体积 JS/CSS 的单页应用中。
常见情况:
- 主 JS 文件加载 404,页面无法执行渲染逻辑;
- 样式文件加载失败,导致 DOM 元素透明或不显示;
- 字体文件加载失败,文字不渲染或闪烁;
- 图片未设置宽高,未加载成功导致布局崩塌;
示例:
- 页面中
main.js
或app.bundle.js
在部署后路径变更,CDN 未同步,返回 404; - 更新版本后清了缓存但加载的是旧 HTML,引用了已不存在的 JS 文件,控制台报错:
GET https://cdn.xxx.com/js/main.123456.js 404
3.2 脚本执行阻塞或异常
即使资源加载成功,如果脚本逻辑出错,仍然会导致页面无法渲染。
常见情况:
- 框架初始化失败(Vue、React 未挂载成功);
- 第三方 SDK 报错(如埋点、广告、支付等)阻断主线程;
- 死循环或严重性能问题导致主线程卡死;
- 异步逻辑处理不当(如 Promise 未 catch);
示例:
useEffect
中 fetch 接口异常未处理,组件直接 returnnull
;- 引入一个广告脚本,脚本中存在
document.write
,导致文档流被清空; - 某个组件初始化时逻辑写法错误,抛出未捕获异常导致整个 React 应用 crash;
3.3 DOM 未挂载或被遮挡
DOM 未挂载或被不可见元素覆盖,是一种非常"隐蔽"的白屏形式 ------ 页面结构存在,但用户无法看到。
常见情况:
v-if
/conditional rendering
控制未命中,页面根组件未渲染;- loading 层
z-index
设置过高,遮住主内容区域; - modal、toast 等组件误操作遮盖整个屏幕;
- DOM 高度为 0、
opacity: 0
或visibility: hidden
;
示例:
- CSS 中某个 reset 样式写错:
css
* { display: none; }
body { visibility: hidden; }
3.4 WebView 容器异常(Hybrid 应用)
Hybrid 应用中,H5 页面运行在 WebView 容器中,若容器环境出现兼容性问题或配置异常,也可能导致页面渲染失败或空白。
常见情况:
- Android 低版本 WebView 渲染失败或白屏;
- iOS WebView 页面切后台再切回来,内容不再刷新;
- JSBridge 注入失败,影响初始化逻辑(如 token 获取、首屏渲染);
- iframe 页面在 WebView 中被 CSP 或
X-Frame-Options
拦截; - WebView 未设置允许 JS 执行或 DOM 存取;
示例:
- 某安卓旧版本机型,首次启动应用加载首页时白屏,刷新后正常 ------ 原因是 WebView 初始化慢,未及时 ready;
- iOS 15 WKWebView 在页面切后台再切回来时内容不刷新,页面卡在首次状态;
- WebView 加载的某个 iframe 页面出现以下错误,导致子内容为空:
Refused to display 'https://xxx.com' in a frame because it set 'X-Frame-Options' to 'deny'.
3.5 逻辑错误或权限控制导致页面空白
页面渲染流程依赖的权限或业务逻辑判断错误,也会造成页面结构不被渲染,导致白屏。
常见情况:
- 用户未登录或未授权,组件直接 return
null
; - 路由守卫逻辑未触发,挂载点未执行;
- 接口未返回或异常,导致视图未进入渲染流程;
- 判断条件过于严格,误判数据不全为"空",不渲染页面内容;
示例:
- React 应用初始化:
tsx
if (!userInfo) return null;
若接口出错,组件直接返回 null,页面完全空白。
- Vue 应用路由守卫逻辑未正确执行,导致主组件未加载:
javascript
router.beforeEach((to, from, next) => {
if (!store.token) return;
// 缺失 next(),页面卡死
});
- 判断数据渲染时没有 fallback:
tsx
return data ? <MainPage /> : null; // 没有 loading 状态
3.6 其他异常情况(如网络超时、缓存问题、安全策略等)
除了常见的代码错误或资源加载问题,现实环境中还有许多非代码层面的异常会导致页面白屏,这类问题通常发生在用户网络、浏览器环境或中间链路,排查难度较大,且大多数无法在开发阶段复现。
常见情况:
-
弱网/断网导致资源卡顿
用户处于 2G/3G 网络、海外网络、网络丢包严重时,页面资源请求超时,长时间 pending。
-
CDN 缓存未刷新
前端部署后未清除旧缓存,导致用户加载了旧的 HTML 文件,而引用的 JS/CSS 已被更新或清理,出现资源 404、页面不渲染。
-
DNS 污染或 TLS 握手失败
DNS 被劫持、污染或解析失败,或 HTTPS 握手过程异常,导致资源加载卡死,白屏时间过长。
-
内容安全策略(CSP)拦截
页面设置了严格的 CSP 策略,导致某些第三方脚本、字体、iframe、图片等被拦截,页面逻辑中断。
-
浏览器设置或扩展限制
用户浏览器禁止 JavaScript 执行或安装了拦截插件(如广告屏蔽、隐私保护插件)导致主逻辑无法运行。
-
HTTP 缓存过期未更新
HTML 返回了缓存的旧版本,但资源文件已清理,导致加载失败。
示例:
-
版本更新后未刷新 HTML 缓存
用户请求了旧 HTML 文件:
html<script src="/assets/main.123456.js"></script>
但实际新版本的构建产物已变为 main.789012.js,导致资源 404,白屏。
-
浏览器 CSP 拦截错误日志
luaRefused to load the script 'https://cdn.thirdparty.com/sdk.js' because it violates the following Content Security Policy directive: "script-src 'self'".
-
使用广告屏蔽插件后页面主结构无法渲染 页面中的主组件引用了被识别为广告资源的关键词,如:
html<div id="ad-wrapper"> ... </div>
被浏览器插件直接移除,导致页面结构断裂。
四、白屏检测思路以及技术方案
白屏检测是一个组合式策略,需要从不同维度(绘制检测、DOM 可见性、性能指标、错误日志、用户交互)联合判断,提高检测准确性与实际可用性。
4.1 检测页面是否绘制有效内容(背景图,DOM挂载点是否有内容)
该策略聚焦于页面是否"真正渲染出内容",不仅关注首次绘制,还关注核心 DOM 节点(如 #app
、#root
)是否被挂载并展示内容。
🌟 实现方式
-
智能根节点识别 :优先识别 React/Vue 常用挂载点(如
#root
,#app
,#main
等),回退到document.body
; -
子元素数量判断:认为渲染结果有效至少要包含一定数量的子节点(比如:阈值 ≥6);
-
可见性判断 :根节点必须可见,利用
getComputedStyle
获取宽高和display/visibility
属性,确保非隐藏(宽高 > 0,display
/visibility
非异常); -
首次内容绘制监测 :通过
PerformanceObserver
获取 FCP 时间;🛠️ 示例代码
javascriptconst rootSelectors = ['#root', '#app', '#main', '#container']; function detectRenderStatus(minChildCount = 2, samplePoints = []) { // 1. 查找挂载根节点 const root = rootSelectors .map(id => document.getElementById(id)) .find(el => el); const el = root || document.body; // 2. 检测子元素数量,可以设定一个阈值: 至少 2 个 const childCount = el.childElementCount; // 3. 检测可见性 const style = window.getComputedStyle(el); const visible = el.offsetWidth > 0 && el.offsetHeight > 0 && style.display !== 'none' && style.visibility !== 'hidden'; // 5. 监听 FCP new PerformanceObserver(list => { for (const entry of list.getEntries()) { if (entry.name === 'first-contentful-paint') { console.log('🎯 FCP:', entry.startTime.toFixed(2), 'ms'); } } }).observe({ type: 'paint', buffered: true });
-
视觉空白点检测(可选) :利用
elementsFromPoint
在画面中心及边缘采样多个点,确保不是"伪白屏"或骨架屏;- 定义判断白屏的"包裹节点":包括文档根
<html>
、<body>
,以及在 H5 中常见承接内容区容器(如 #container、.content)。 - 9 个横向 + 9 个纵向采样:共 18 个点
(还可以交叉选取 && 垂直交叉选取)
,通过 elementsFromPoint 获取视口关键区域上的顶层元素。 - 统计空白点:若采样点返回的元素为 wrapper(只是最基本结构,无实际内容),则认为该点为空。
- 阈值判断:当大多数采样点(>16)都对应的是 wrapper 节点时,几乎可以确定页面存在白屏。
- 上报白屏:获取中心采样点元素 selector、窗口与屏幕尺寸,调用 tracker.send() 进行监控上报。
🛠️ 示例代码
- 定义判断白屏的"包裹节点":包括文档根
javascriptexport function blankScreen() { // 👉 1. 定义白屏判断中"wrapper"元素,即最顶层的空节点容器 const wrapperElements = ['html', 'body', '#container', '.content']; // 用于统计在采样点上检测到"空白包装节点"的次数 let emptyPoints = 0; /** * 获取目标元素的选择器字符串 * @param {Element} element * @returns {string} 例如 '#app', '.main', 'div' */ function getSelector(element) { if (!element) return ''; if (element.id) { // 有 ID 就返回 "#id" return '#' + element.id; } else if (element.className) { // 多 class 转成 ".class1.class2" return ( '.' + element.className .split(' ') .filter(item => item) // 过滤掉空字符串 .join('.') ); } else { // 没有 id 或 class,用标签名小写 return element.nodeName.toLowerCase(); } } /** * 判断元素是否属于包裹(wrapper)节点 * 如果是,就将 emptyPoints 累加 */ function isWrapper(element) { const selector = getSelector(element); if (wrapperElements.indexOf(selector) !== -1) { emptyPoints++; } } /** * 📌 onload 意味着视图和资源加载完成后才会执行检测逻辑 */ onload(function () { // 2. 拿 9 个点横向 + 9 个点纵向做采样,一共 18 次检测 for (let i = 1; i <= 9; i++) { // 横向采样点:屏幕宽度的 1/10、2/10 ... 9/10,垂直中心 const xElements = document.elementsFromPoint( (window.innerWidth * i) / 10, window.innerHeight / 2 ); // 纵向采样点:屏幕高度的 1/10、2/10 ... 9/10,水平中心 const yElements = document.elementsFromPoint( window.innerWidth / 2, (window.innerHeight * i) / 10 ); // 判断每个采样点上的第一个元素是否为"wrapper" isWrapper(xElements[0]); isWrapper(yElements[0]); } // 3. 如果 wrapper 探测次数超过 16(即大部分点都只命中 html/body 等空白层) if (emptyPoints > 16) { const centerElement = document.elementsFromPoint( window.innerWidth / 2, window.innerHeight / 2 )[0]; // 上报白屏数据,通过 tracker SDK、或者通过Sentry 日志上报 tracker.send({ kind: 'stability', // 类别:稳定性 type: 'blank', // 类型:白屏 emptyPoints, // 空白点数量 screen: window.screen.width + '*' + window.screen.height, // 物理屏幕分辨率 viewPoint: window.innerWidth + '*' + window.innerHeight, // 可视窗口尺寸 selector: getSelector(centerElement) // 中心点元素的选择器,用于定位问题 }); } }); }
-
4.2 页面加载资源检测 && 浏览器性能指标监测
白屏问题频繁源于关键资源加载失败或延迟过高,特别是在webview 当中,以 JS、CSS、图片及字体为代表的资源在首屏体验中扮演重要角色。如果加载资源过大,或者加载的样式失败,也会导致白屏时间过长,或者直接白屏,以下内容细化检测思路和实践方式。
4.2.1 🧠 检测思路
- 监控关键资源是否 加载失败(404、CSP 拦截等);
- 跟踪资源加载 时间 和 大小,识别阻塞下载的资源;
- 判断这些资源是否影响首屏渲染路径。
4.2.2 关键资源识别
- 核心JS/CSS:如框架代码、业务主包、首屏渲染依赖
- 关键图片/字体:首屏可见区域内的图片和自定义字体
- 第三方依赖:CDN资源、SDK等
4.2.3 资源加载失败监听
js
window.addEventListener('error', e => {
const t = e.target || e.srcElement;
if (t instanceof HTMLScriptElement || t instanceof HTMLLinkElement) {
console.error('Resource failed:', t.tagName, t.src || t.href);
reportResourceError({
type: t.tagName,
url: t.src || t.href,
timestamp: Date.now()
});
}
}, true);
4.2.4 Performance API 获取资源加载信息
js
const entries = performance.getEntriesByType('resource');
entries.forEach(r => {
// 典型字段:r.name, r.initiatorType ('script', 'css', 'img', 'font'), r.duration, r.transferSize
if ((r.initiatorType === 'script' || r.initiatorType === 'css' || r.initiatorType === 'font' || r.initiatorType === 'img') && r.duration > 2000) {
console.warn('Slow resource:', r.name, r.duration);
reportSlowResource({ url: r.name, duration: r.duration });
}
});
小结:
- 根据域名/路径标记首屏关键 JS、CSS资源;
- 对体积较大的图片、字体文件设置加载时间或大小阈值;
- 与 FCP / DOM 检测结果联合判断,若关键资源加载慢或失败且未触发 FCP,则可能白屏。
4.3 页面渲染截图分析
利用Puppeteer
对页面在合适的时机(FCP,LCP,onload ,document.readyState,DOMContentLoaded 等)进行截图,通过分析页面实际渲染效果的截图,检测是否呈现"真正的内容",可以有效识别骨架屏或资源加载失败导致的白屏。
javascript
const puppeteer = require('puppeteer');
// LCP 监听函数,将页面窗口挂载 largestContentfulPaint 值
const LCP_SCRIPT = `
window.largestContentfulPaint = 0;
const po = new PerformanceObserver(list => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
window.largestContentfulPaint = last.renderTime || last.loadTime;
});
po.observe({ type: 'largest-contentful-paint', buffered: true });
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
po.takeRecords();
po.disconnect();
}
});
`;
/** 在浏览器端注入 LCP 监听逻辑 */
async function installLCP(page) {
await page.evaluateOnNewDocument(LCP_SCRIPT);
}
async function captureAtPoints(url, device) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
if (device) await page.emulate(device);
await installLCP(page);
// 1. DOMContentLoaded 触发截图
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
const domReadyShot = await page.screenshot({ fullPage: true });
console.log('[✳️] DOMContentLoaded screenshot taken');
// 2. networkidle2 或 load 时机截图,并后置延时
await page.goto(url, { waitUntil: 'load', timeout: 60000 });
await page.waitForTimeout(1000);
const onLoadShot = await page.screenshot({ fullPage: true });
console.log('[✅] load screenshot taken');
// 3. 获取 LCP 值
const lcp = await page.evaluate(() => window.largestContentfulPaint);
console.log('🏁 Captured LCP:', lcp, 'ms');
await browser.close();
return { domReadyShot, onLoadShot, lcp };
}
/** 白屏像素分析,用于 onload 时的截图 */
function analyzeWhiteScreen(buffer) {
return new Promise(resolve => {
const img = new Image();
img.src = 'data:image/png;base64,' + buffer.toString('base64');
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, img.width, img.height).data;
const total = img.width * img.height;
let whites = 0;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 240 && data[i+1] > 240 && data[i+2] > 240) whites++;
}
resolve(whites / total);
};
});
}
// 主流程,依次执行、检测
(async () => {
const { KnownDevices } = require('puppeteer');
const devices = [KnownDevices['iPhone 13 Pro'], KnownDevices['Pixel 6'], null];
for (const device of devices) {
const { onLoadShot, lcp } = await captureAtPoints('https://example.com', device);
const ratio = await analyzeWhiteScreen(onLoadShot);
console.log(`Device ${device?.name || 'Desktop'} white pixel ratio:`, ratio.toFixed(3));
if (ratio >= 0.95) {
console.warn(`⚠️ White-screen detected at onload on ${device?.name || 'Desktop'}`);
console.log('📊 LCP was', lcp, 'ms -- helps判断是否是性能问题');
}
}
})();
4.4 利用AI对截图进行识别
可以白屏检测中引入 AI 分析技术,以下示例展示了如何调用 OpenAI GPT-4 Vision API 对截图进行内容识别,从而判断页面是否呈现"真实内容"或仍是白屏。 您可以将 Puppeteer 生成的截图转为 Base64 后,发送到 GPT-4 Vision 端点,由模型识别图像内容并返回文字描述,辅助进行白屏判断。
🛠 Node.js 调用示例
javascript
import fs from 'fs';
import path from 'path';
import OpenAI from 'openai';
// 初始化 OpenAI 客户端(需设置环境变量 OPENAI_API_KEY)
const openai = new OpenAI();
// 加载本地 onload 时段截图(示例为 PNG 格式)
const imgPath = path.resolve(__dirname, 'onload.png');
const imgBase64 = fs.readFileSync(imgPath, { encoding: 'base64' });
// 调用 GPT-4 Vision 识别接口识别图片内容
async function analyzeScreenshotWithAI() {
const response = await openai.chat.completions.create({
model: 'gpt-4o', // GPT-4o 支持图像输入,也可使用 GPT‑4V 模式
messages: [
{ role: 'system', content: '你是图片识别助手,判断屏幕是否已经渲染出可见页面内容。' },
{
role: 'user',
content: [
{ type: 'text', text: '请识别下面图片是否为网页内容(非白屏):' },
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imgBase64}` } }
]
}
],
max_tokens: 500,
});
const reply = response.choices[0].message.content;
console.log('GPT‑4 Vision 识别结果:', reply);
// 简单判断逻辑:若包含"白屏"、"空白"相关描述即判定为白屏
const isBlank = /白屏|空白|无内容/.test(reply);
console.log('识别判断是否白屏:', isBlank);
}
analyzeScreenshotWithAI().catch(console.error);
4.5 状态码检测
在 Web 页面加载过程中,即使 HTML 正常返回,也可能存在服务端状态异常、权限错误、或业务异常文案未渲染,导致用户实际看到"白屏"或"报错页"。或者像我们SSR 渲染的页面白屏可能访问页面链接返回的是404状态
因此除了 DOM 渲染检测,我们还需要结合:
- 📦 HTTP 状态码
- 📄 页面内容文案
- 🧠 业务逻辑判断
来进行更准确的"白屏/异常页"识别与上报。
通过服务端日志监控,检测请求页面时候响应状态码,如:
403 Forbidden
权限错误404 Not Found
页面不存在500 Internal Server Error
服务异常503 Service Unavailable
服务不可用
或者通过JS检测页面所有节点中,是否有报错的文案,来识别
示例代码
javascript
// 引入 Sentry(确保你项目中已初始化 Sentry)
import * as Sentry from '@sentry/browser';
// 定义要检测的关键词数组
const errorKeywords = ['404', '页面不存在', '服务器错误', '出错了', '系统异常'];
// 获取页面中所有元素节点
const allElements = document.body.getElementsByTagName('*');
for (let i = 0; i < allElements.length; i++) {
const el = allElements[i];
// 获取当前节点的文本内容(去除空格)
const text = el.innerText?.trim();
if (!text) continue;
for (let j = 0; j < errorKeywords.length; j++) {
const keyword = errorKeywords[j];
if (text.includes(keyword)) {
console.warn('检测到错误关键词:', keyword, '于节点:', el);
// 使用 Sentry 上报自定义异常
reportWhiteScreenWithSentry({
reason: 'error_keyword_matched',
keyword,
textContent: text,
tag: el.tagName,
url: location.href,
time: Date.now()
});
return;
}
}
}
// 使用 Sentry 上报白屏或异常文案
function reportWhiteScreenWithSentry(data) {
Sentry.captureMessage('页面出现业务级错误文案', {
level: 'error',
tags: {
errorType: 'business_white_screen',
keyword: data.keyword,
tagName: data.tag
},
extra: {
textContent: data.textContent,
url: data.url,
time: data.time
}
});
}
五、上报机制与监控实践
5.1 上报字段设计
为了准确定位和分析白屏问题,上报数据结构中应包含全面而精细的字段,支持后续聚合、告警和排查。
🎯 基础字段(RUM + 白屏维度)
字段名 | 类型 | 描述 |
---|---|---|
eventId | string | 全局唯一事件 ID,用于关联多条日志 |
timestamp | number | 上报时间戳(ms 精度) |
url | string | 当前页面 URL |
referer | string | 页面来源地址 |
userAgent | string | 浏览器 UA 信息 |
screenWidth/Height | number | 屏幕物理分辨率 |
viewportWidth/Height | number | 实际可见区域尺寸 |
⚙️ 性能指标字段
字段 | 类型 | 描述 |
---|---|---|
fcp | number | First Contentful Paint 时间(ms) oai_citation:0‡mo4tech.com |
lcp | number | Largest Contentful Paint 时间(ms)() |
domContentLoaded | number | DOMContentLoaded 事件触发时间(ms) |
loadEvent | number | window.onload 触发时间(ms) |
🕵️ 可视/白屏判断字段
字段 | 类型 | 描述 |
---|---|---|
blankScreenshotRatio | number | onload 时截图中白色像素比例(0~1) |
blankSamplePoints | number | DOM 可视检测中空白采样点数量 |
blankRootChildCount | number | 根挂载节点(如 #app )的子节点数量 |
blankRootVisible | boolean | 根挂载节点是否可见(宽高 >0 且非不可见 CSS) |
🧩 资源/异常字段
字段 | 类型 | 描述 |
---|---|---|
resourceErrors | array | 资源加载失败的 URL 列表(JS/CSS/图片) |
slowResources | array | 加载时间超过阈值的资源(含 URL、duration) |
jsErrorsCount | number | window.onerror 捕获的 JS 错误数量 |
unhandledRejections | number | 未捕获 Promise 异常数量 |
🌐 环境上下文字段
networkType
:如4g
,3g
,wifi
connectionDownlink
:带宽 Mbpsos
,browserName
,browserVersion
appVersion
,releaseStage
(如production
/staging
)location
或locale
(可选区分地区)
用户信息字段
字段 | 类型 | 描述 |
---|---|---|
deviceInfo | Object | 登录的用户信息/设备信息 |
📋 示例上报 JSON
json
{
"eventId": "abc123",
"timestamp": 1710102030000,
"url": "https://example.com/home",
"userAgent": "...",
"screenWidth": 390, "screenHeight": 844,
"viewportWidth": 390, "viewportHeight": 844,
"fcp": 1200,
"lcp": 2500,
"domContentLoaded": 800,
"loadEvent": 3000,
"blankScreenshotRatio": 0.97,
"blankSamplePoints": 17,
"blankRootChildCount": 0,
"blankRootVisible": false,
"resourceErrors": ["https://cdn/a.js"],
"slowResources": [{"url":"...","duration":4000}],
"jsErrorsCount": 2,
"unhandledRejections": 1,
"networkType": "4g",
"os": "iOS",
"browserName": "Safari",
"appVersion": "v1.2.3",
"releaseStage": "production",
"deviceInfo": {
}
}
5.2 如何在项目中对业务白屏
上报
- 区别于"技术白屏"(如 DOM 渲染失败、JS 错误),业务白屏是:
- 页面 DOM 已渲染,但核心业务模块未展示(如:内容为空、卡片缺失、数据列表加载失败等)
- 多数由数据加载失败、接口异常、权限问题、逻辑缺陷等引起
- ErrorBoundary (错误边界)是 React 提供的一种机制,用于捕获其子组件在 渲染期间 、生命周期方法中 、构造函数中 以及 事件处理之外的错误,防止整个 React 应用崩溃。
⚠️ 注意:它无法捕获:
- 事件处理器中的错误(需要自己 try/catch)
- 异步代码(如
setTimeout
、Promise
)- 服务端渲染时的错误(上报后端日志)
- 自身的错误(ErrorBoundary 组件本身的错误)
tsx
import React from 'react';
import * as Sentry from '@sentry/react';
// 适用于 React <19 或对错误上报有特殊需求的场景
export class CustomErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {
// 上报 Sentry,附带组件调用栈信息
Sentry.captureReactException(error as Error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>组件出错误了 ~</div>; // 对出错界面兜底,避免直接是空白的尴尬
}
return this.props.children;
}
}
5.3 通知与告警
利用webhook 接入钉钉/企业微信群通知,分配给对应的人去解决
六、Demo 展示
以火花业务组白屏检测脚本为列:
监控目标 :
目标详情 :
任务列表 :
任务详情 :