白屏检测技术分享

一、背景与意义

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渲染;

💡 举例说明:

  1. 首屏加载资源失败

    HTML 加载成功,但主 JS 文件因 CDN 故障加载失败,React 应用无法挂载,页面空白。

  2. Vue 应用逻辑中断

    接口鉴权失败,导致未进入 router.beforeEach 的回调,主页面未渲染,页面为空。

  3. WebView 特定版本兼容问题

    某版本 iOS WKWebView 渲染失败,页面明明加载完成但屏幕空白(崩溃或者闪退)。

  4. 懒加载模块未触发

    首页使用异步组件按需加载,但逻辑判断错误,组件未被实际加载渲染。

  5. 被全屏 loading 覆盖
    v-if="isLoading" 控制的全屏 loading 层未正确置为 false,导致主页面内容永远被遮住。


2.2 白屏与骨架屏、加载状态的区别

白屏并不等于页面"尚未加载完成"。在良好体验设计中,常见的还有"骨架屏"和"loading 状态",它们虽也未展示最终内容,但不会给用户"页面坏了"的感受。

类型 是否可视 用户感知 页面状态 是否可接受
❌ 白屏 页面打不开、出错 无 DOM 或 DOM 不可见
✅ 骨架屏 页面正在加载中 结构占位已渲染 是 ✅
✅ loading 动画 页面正在初始化中 通常为动画遮罩层 是 ✅
⚠️ 伪白屏 内容加载中但看不到 内容被遮挡或透明 否(体验差)
🤔 业务白屏 页面有内容 内容渲染异常不符合预期 否(体验差)

🆚 差异解析:

  • 白屏:页面真正没有任何可见的结构内容,或者有但用户看不到(例如样式未加载、透明、被遮挡),属于严重问题。
  • 骨架屏:一种友好的过渡设计,通过骨架结构占位减少用户等待焦虑,告诉用户"页面正在努力加载"。
  • Loading 状态:通常表现为转圈动画、进度条等,表示页面还在执行初始化逻辑,但不会空白。
  • 伪白屏:表面上是"白",但其实页面结构在,只是被遮住或逻辑未更新,属于"误导型白屏"。
  • 业务白屏:页面展示通用的报错兜底界面或者如404,502 这种页面"。

💡 举例说明:

  1. 骨架屏正常示例

    京东、淘宝等电商首页加载时显示骨架图 ------ 左侧商品图、右侧文字块模拟,2 秒后替换为真实内容。

  2. 伪白屏示例

    页面加载成功但样式加载失败,所有元素在但 display: none;或全屏 loading 元素未隐藏,导致主内容被永久遮盖。

  3. Loading 正常示例

    微信小程序加载页面时转圈,同时底部显示"加载中,请稍候..."。

  4. 业务白屏示例

    页面渲染异常,如页面展示404,502 等状态码,或者接口鉴权失败等,服务端数据返回有问题,页面展示"出错了"等兜底内容。


✅ 小结

  • 真正的白屏,是页面内容从用户角度看"不存在",业务白屏用户能看到,但是不符合预期,影响用户体验;
  • 骨架屏与 loading 动画是"可控"的等待体验;
  • 白屏需要技术手段监测和修复,骨架和 loading 是设计手段预防"白屏感";
  • 有效地区分这些状态有助于提升体验监控精度,避免误判。

三、出现白屏的原因

3.1 资源加载失败(404)

资源加载失败是最常见的白屏诱因之一,尤其是在首屏强依赖大体积 JS/CSS 的单页应用中。

常见情况:

  • 主 JS 文件加载 404,页面无法执行渲染逻辑;
  • 样式文件加载失败,导致 DOM 元素透明或不显示;
  • 字体文件加载失败,文字不渲染或闪烁;
  • 图片未设置宽高,未加载成功导致布局崩塌;

示例:

  • 页面中 main.jsapp.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 接口异常未处理,组件直接 return null
  • 引入一个广告脚本,脚本中存在 document.write,导致文档流被清空;
  • 某个组件初始化时逻辑写法错误,抛出未捕获异常导致整个 React 应用 crash;

3.3 DOM 未挂载或被遮挡

DOM 未挂载或被不可见元素覆盖,是一种非常"隐蔽"的白屏形式 ------ 页面结构存在,但用户无法看到。

常见情况:

  • v-if / conditional rendering 控制未命中,页面根组件未渲染;
  • loading 层 z-index 设置过高,遮住主内容区域;
  • modal、toast 等组件误操作遮盖整个屏幕;
  • DOM 高度为 0、opacity: 0visibility: 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 拦截错误日志

    lua 复制代码
    Refused 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 时间;

    🛠️ 示例代码

    javascript 复制代码
    const 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() 进行监控上报。

      🛠️ 示例代码

    javascript 复制代码
      export 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:带宽 Mbps
  • os, browserName, browserVersion
  • appVersion, releaseStage(如 production/staging
  • locationlocale(可选区分地区)

用户信息字段

字段 类型 描述
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 如何在项目中对业务白屏上报

  1. 区别于"技术白屏"(如 DOM 渲染失败、JS 错误),业务白屏是:
  • 页面 DOM 已渲染,但核心业务模块未展示(如:内容为空、卡片缺失、数据列表加载失败等)
  • 多数由数据加载失败、接口异常、权限问题、逻辑缺陷等引起
  1. ErrorBoundary (错误边界)是 React 提供的一种机制,用于捕获其子组件在 渲染期间生命周期方法中构造函数中 以及 事件处理之外的错误,防止整个 React 应用崩溃。

⚠️ 注意:它无法捕获:

  • 事件处理器中的错误(需要自己 try/catch)
  • 异步代码(如 setTimeoutPromise
  • 服务端渲染时的错误(上报后端日志)
  • 自身的错误(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 展示

以火花业务组白屏检测脚本为列:

监控目标

目标详情

任务列表

任务详情

参考文档:

页面白屏如何排查 白屏检测小工具 Page Probe - 落地页性能监控系统

相关推荐
I'mxx1 分钟前
【html常见页面布局】
前端·css·html
万少6 分钟前
云测试提前定位和解决问题 萤火故事屋 上架流程
前端·harmonyos·客户端
倔强青铜三21 分钟前
苦练Python第22天:11个必学的列表方法
人工智能·python·面试
倔强青铜三24 分钟前
苦练Python第21天:列表创建、访问与修改三板斧
人工智能·python·面试
brzhang44 分钟前
OpenAI 7周发布Codex,我们的数据库迁移为何要花一年?
前端·后端·架构
军军君011 小时前
基于Springboot+UniApp+Ai实现模拟面试小工具三:后端项目基础框架搭建上
前端·vue.js·spring boot·面试·elementui·微信小程序·uni-app
布丁05231 小时前
DOM编程实例(不重要,可忽略)
前端·javascript·html
bigyoung1 小时前
babel 自定义plugin中,如何判断一个ast中是否是jsx文件
前端·javascript·babel
指尖的记忆2 小时前
当代前端人的 “生存技能树”:从切图仔到全栈侠的魔幻升级
前端·程序员
草履虫建模2 小时前
Ajax原理、用法与经典代码实例
java·前端·javascript·ajax·intellij-idea