从方案到原理,带你从零到一实现一个 前端白屏 检测的 SDK ☺️☺️☺️

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

前端白屏是指用户打开网页时,页面未能正常加载或渲染,导致浏览器显示一片空白。这通常是由于 JavaScript 错误、资源加载失败、网络问题或渲染逻辑错误引起的。尤其在单页面应用(SPA)中,前端白屏问题变得更加复杂,可能导致用户无法看到任何有效内容。解决白屏问题需要快速定位并修复错误,确保资源正确加载和渲染。

白屏的表现和原因

白屏的表现通常表现为以下几种情况:

  1. 页面空白:用户打开页面时,只能看到一个空白的浏览器窗口,没有任何内容显示。页面完全没有渲染,背景通常是白色的。

  2. 加载中状态无变化:页面在加载过程中,可能显示一个加载动画或进度条,但这个动画或进度条会停滞不前,长时间没有进展,最终变成空白。

  3. 部分元素未显示:有时页面的某些内容未能渲染出来,可能只有一部分元素(如背景或某些框架)显示,其他部分完全消失,形成一个不完整的页面。

  4. 浏览器错误提示:在开发环境下,开发者可能看到浏览器的控制台报错信息(如 JavaScript 错误、资源加载失败等),但用户端则仅显示空白页面。

  5. 不响应交互操作:页面空白且无法进行任何交互。用户尝试点击或滚动页面,但页面没有任何反应。

  6. 无状态或黑屏:不是完全的白色屏幕,而是页面没有任何内容,背景可能变为黑色或灰色,但这通常表示页面渲染异常。

导致白屏的原因分为两种:资源加载错误、代码执行错误。

资源加载错误

资源加载错误通常指页面依赖的静态文件或网络请求未能成功加载,导致页面无法显示内容。常见的资源加载错误有:

  1. JavaScript 文件加载失败:如果必需的 JavaScript 文件未能加载(可能是文件路径错误、网络问题、服务器故障等),页面中的动态功能无法执行,导致空白。

  2. CSS 文件加载失败:如果页面样式表加载失败,页面元素的布局和样式无法应用,可能导致页面看起来是空白的。

  3. 图片、字体等静态资源加载失败:如果页面依赖的图片、字体等静态资源加载失败,可能导致页面内容不完整,甚至出现空白。

  4. API 请求失败:如果页面需要通过 API 获取数据,而 API 请求失败或返回错误(如 404 或 500 错误),可能导致页面的动态内容无法渲染。

代码执行错误

代码执行错误是指 JavaScript 脚本在浏览器中运行时出现问题,导致页面的渲染过程被中断。常见的代码执行错误有:

  1. JavaScript 运行时错误:在页面的脚本执行过程中,可能会出现未捕获的异常(例如访问未定义的变量、调用不存在的函数等),这些错误会阻止后续代码的执行,导致页面无法渲染。

  2. 框架渲染错误:在使用像 React、Vue 或 Angular 等前端框架时,如果框架的组件渲染出现错误(如组件数据异常、状态管理错误等),也可能导致页面白屏。

  3. 异步代码问题:前端代码中大量依赖异步操作(例如使用 fetch 或 axios 发起 API 请求),如果这些异步操作失败(如网络问题、服务器返回错误等),也可能导致页面渲染失败。

  4. 无限循环或死锁:前端代码中的逻辑错误(如无限递归、死循环等)会使 JavaScript 引擎崩溃或挂起,导致页面无法渲染。

检测方案对比

方法 原理 优点 缺点
检测某节点是否挂载 SPA 框架渲染的 DOM 一般挂载在一个根节点下,监听 onload, onerror 事件,检测根节点是否挂载 DOM 开发成本低 通用性差,只兼容主流 SPA 框架
监听 DOM 变化 利用 Mutation Observer API 监听 DOM 变化 开发成本较低 准确度低,无法检测未渲染。结束渲染后可能丢失状态,如果用户长时间未操作 DOM 可能会失效
页面截图对比 对页面截图,将页面与先前的截图进行对比 技术难度无,适用性好 准确度低,无法检测到背景图、引导屏幕的背景
前端错误内存溢出监测 利用 ErrorBoundary 组件捕获 JS 异常并检测页面异常 开发成本较低 无法检测页面异常屏幕,只能检测框架程度
页面元素键值对比 在页面中查看交互/交叉验证各种样式,使用 elementsFromPoint API 获取元素下的信息 准确性高,技术难度低 开发成本较高

elementsFromPoint 是一个 JavaScript 的 API,用于获取指定屏幕坐标位置上所有的 DOM 元素。通过这个方法,你可以获得给定坐标处所有的元素,按层叠顺序(从上到下)。

如下代码使用示例:

JS 复制代码
document.elementsFromPoint(x, y);

这几种前端白屏检测方案的主要区别在于检测原理和适用场景:

  1. 检测节点是否挂载 主要通过监听页面根节点的事件来判断页面是否加载成功,适用于简单的 SPA 框架,但通用性差。

  2. 监听 DOM 变化 利用 MutationObserver API 动态监控 DOM 变化,适用于动态渲染,但准确度较低,无法检测未渲染的内容。

  3. 页面截图对比 通过对比当前页面截图与预期截图来判断页面是否正确渲染,适用于检测页面的完整性,但无法处理动态内容。

  4. 前端错误监测 通过捕获 JavaScript 错误来检测页面崩溃,适用于框架中的错误检测,但无法捕获页面显示异常。

  5. 页面元素对比 利用 elementsFromPoint API 获取页面元素信息,检查页面显示,精确度高,但开发成本较高,适合复杂页面。

每种方法在精确度、开发难度和适用范围上有所不同。

通过以上对比发现,采用 页面关键点采样对比 的实现方案较好。

需要注意的是,对于主应用内嵌入的 iframe 的场景,因为每次采样取到的都是整个 iframe 元素,所以无法在主应用侧判断 iframe 是否白屏,需要在 iframe 应用内接入白屏检测 SDK。

数据采集

这个流程图详细描述了前端白屏检测的步骤,尤其考虑到骨架屏应用的特殊情况。首先,系统会判断页面是否是骨架屏应用。这是因为骨架屏是一种页面加载过程中的占位符,通常会显示一个简单的框架或动画,以给用户提示页面正在加载。骨架屏本身可能会展示一个白屏状态,因此需要额外判断,确保不是因为骨架屏造成的误判。

如果页面是骨架屏应用,接着会检查页面是否加载完成或出现加载错误。如果页面加载完成且没有错误,系统会在浏览器空闲时进行屏幕采样,判断当前页面是否满足白屏标准。如果符合标准,表示页面可能处于白屏状态,系统进一步判断是否开启了轮询检测。如果未开启,系统将启动轮询检测;如果已经开启,系统会比较初次采样结果与后续结果是否一致,确保页面没有发生变化。如果结果不一致,说明页面可能出现了白屏,系统将上报错误。

整个流程通过这些判断步骤,确保能够正确识别和上报白屏问题,尤其是在骨架屏这种特殊情况下,避免误判。

屏幕采样点选取

采样点的选取有三种方式:垂直采样、交叉采样、垂直交叉采样。

垂直采样

垂直采样是指从页面的顶部到底部沿垂直方向进行采样,逐行或以一定间隔采样页面的内容。这种方法可以简单而有效地检测页面的渲染情况,特别适用于页面内容有明显的垂直布局(如文章、博客等)。

但是如果页面的内容横向布局或者大量动态加载的部分,垂直采样可能无法完全覆盖页面的渲染状态。

交叉采样

交叉采样是指从页面的多个位置同时进行采样,既涵盖垂直方向,也涉及水平方向的多个位置。通常,它通过交替选择页面不同区域的采样点来进行检测。交叉采样能够捕捉更多页面细节,特别适用于多列布局或复杂页面结构(如新闻网站、电子商务平台等),可以更全面地反映页面的渲染情况。与垂直采样相比,交叉采样需要更多的计算资源,采样点较多,因此在实现上更复杂,可能会带来额外的性能开销。

垂直交叉采样

垂直交叉采样结合了垂直采样和交叉采样的特点,既沿页面的垂直方向从顶部到底部进行采样,又在不同的垂直线间交替进行水平采样。这样可以覆盖页面更广泛的区域,既能检查整体布局,又能捕捉细节,适用于复杂的页面结构。由于采样点增多,计算量较大,可能对性能造成一定压力,因此在实际应用中需要平衡检测精度与性能开销。

为了克服垂直交叉采样带来的性能问题,我们可以利用 requestIdleCallback 在浏览器空闲时执行计算。由于 requestIdleCallback 只在空闲时段运行,它不会阻塞页面渲染或影响用户操作的响应速度,从而有效减轻了计算负担。

白屏的判断标准和检测时机

有骨架屏和无骨架屏应用的检测方式不一样,检测时机也有细微差别。

无骨架屏场景

确定可见元素的检测时机是一个关键挑战,时机把握不当容易导致误判。如果检测过早,可能会将页面加载过程中的短暂空白误认为是白屏,从而无法准确反映页面最终的渲染状态;而如果检测过晚,当用户因长时间等待而关闭页面时,白屏情况就会错过,导致漏测。因此,合理选择检测时机至关重要,既要避免过早判断,也要防止延迟导致漏报。

检测时机

检测的时机主要分为以下几个方面:

  1. 页面加载完成后:在页面加载完成并没有出现任何加载错误时,监控会开始进行初始的采样。startMonitoring 方法作为监控的入口,会在方法中调用 requestIdleCallback 来进行浏览器空闲时的采样。

  2. 浏览器空闲时采样:通过使用 requestIdleCallback 来执行采样任务,确保采样操作不会阻塞页面渲染和用户交互。requestIdleCallback 确保在浏览器空闲时执行,从而提升性能并减少对用户体验的影响。

  3. 页面内容变化检测:在每次采样时,系统会将当前的采样数据与初始采样数据进行对比,检查页面是否发生变化。如果页面发生了变化,系统会进行异常上报(reportAnomaly 方法),并启动重试机制(retrySampling 方法)。

  4. 白屏检测:系统每次采样时都会判断页面中是否有空白区域。若空白区域超过设定的阈值(threshold),则认为页面为白屏并触发上报。白屏检测基于页面元素的有无和可视状态进行判定,通过大量的采样点来增强准确性。

  5. 重试机制:在页面发生变化或者检测到白屏的情况下,系统会启动重试机制。通过 retrySampling 方法,系统会根据设定的最大重试次数和重试间隔进行多次采样,直到检测到页面发生变化或达到最大重试次数。

  6. 停止监控:在监控过程中,如果页面发生特殊情况(例如,页面请求被取消,或者页面被强制停止),可以调用 stopMonitoring 方法来停止监控。调用时,requestIdleCallback 会停止采样,监控任务也会被清除,避免不必要的资源消耗。

检测方式

无骨架屏的检测方式主要有以下几个方面:

  1. 初始化和配置根容器:在初始化 SDK 时,我们需要根据页面的结构配置根容器。如果根容器为空(即页面中没有有效的容器元素),则认为页面发生了白屏。在代码中,根容器元素的判断通过 isContainer 方法实现。该方法通过判断元素的 tagName 是否为常见容器元素(如 DIV, SECTION, MAIN, HEADER, FOOTER)来确定是否是有效的根容器。

  2. 采样点的计算和检测:通过获取页面的宽度 (window.innerWidth) 和高度 (window.innerHeight),代码计算了多个采样点的坐标,涵盖了页面的各个方向(包括 X 轴、Y 轴、上升对角线和下降对角线)。使用 document.elementsFromPoint 方法获取每个坐标的 DOM 元素,并判断这些元素是否为有效的根容器元素。若某些采样点没有有效的容器元素,便增加空点计数。

  3. 白屏检测:每次采样后,系统会检查页面中是否有空白区域。空白区域超过设定的阈值(threshold)时,认为页面发生了白屏并进行上报。为了避免遗漏微前端或 iframe 场景的子应用,代码选定页面的 右下方 作为内容区,通过检查该区域内的采样点来判断是否满足白屏条件。这样可以确保即使主应用中没有白屏,子应用的白屏问题也能够被检测到。

  4. 重试机制:在检测到页面内容发生变化或白屏时,系统会启动重试机制。通过 retrySampling 方法,系统会在设定的最大重试次数和重试间隔内进行多次采样,直到页面状态发生变化或达到最大重试次数。此机制确保了即使在页面动态变化时,监控也能够及时捕捉到状态变化。

  5. 停止监控:当页面发生特殊情况(如请求被取消或页面被强制停止)时,监控会被停止。通过 stopMonitoring 方法,requestIdleCallback 会停止采样任务,清除定时器,避免不必要的资源消耗。

如上图所示,整个屏幕共 33 个采样点,其中内容区有 28 个。简单起见,检测白屏时,我们判断空白的采样点是否大于等于 28 个。采样点坐标的获取如下:

ts 复制代码
for (let i = 1; i <= 9; i++) {
  // x轴采样点
  const xElements = document?.elementsFromPoint(
    (window.innerWidth * i) / 10,
    window.innerHeight / 2
  );
  // y轴采样点
  const yElements = document?.elementsFromPoint(
    window.innerWidth / 2,
    (window.innerHeight * i) / 10
  );
  // 上升的对角线采样点
  const upDiagonalElements = document?.elementsFromPoint(
    (window.innerWidth * i) / 10,
    (window.innerHeight * i) / 10
  );
  // 下降的对角线采样点
  const downDiagonalElements = document?.elementsFromPoint(
    (window.innerWidth * i) / 10,
    window.innerHeight - (window.innerHeight * i) / 10
  );

  // 针对每个方向的采样进行数据获取
  sampleData[`xElement_${i}`] = this.getElementSample(xElements[0]);
  sampleData[`yElement_${i}`] = this.getElementSample(yElements[0]);
  sampleData[`upDiagonalElement_${i}`] = this.getElementSample(
    upDiagonalElements[0]
  );
  sampleData[`downDiagonalElement_${i}`] = this.getElementSample(
    downDiagonalElements[0]
  );

  // 判断是否为空点(无有效内容或不可见)并同时检查是否是容器
  if (
    !this.isElementVisible(xElements[0]) ||
    !this.isContainer(xElements[0] as HTMLElement)
  )
    this.emptyPoints++;

  if (i !== 5) {
    // 避免中心点重复计算
    if (
      !this.isElementVisible(yElements[0]) ||
      !this.isContainer(yElements[0] as HTMLElement)
    )
      this.emptyPoints++;
    if (
      !this.isElementVisible(upDiagonalElements[0]) ||
      !this.isContainer(upDiagonalElements[0] as HTMLElement)
    )
      this.emptyPoints++;
    if (
      !this.isElementVisible(downDiagonalElements[0]) ||
      !this.isContainer(downDiagonalElements[0] as HTMLElement)
    )
      this.emptyPoints++;
  }
}

骨架屏场景

检测的时机主要分为以下几个方面:

  1. 监听骨架屏消失:使用 MutationObserver 监听 DOM 变化,当骨架屏从页面中移除时,触发白屏检测。

  2. 超时机制:如果骨架屏在 skeletonMaxWaitTime 内没有消失,强制触发白屏检测,防止页面无限等待。

  3. 骨架屏消失后检测白屏:当骨架屏移除后,立即检查页面是否有有效内容,确保页面正确渲染。

检测方式

如果应用内有骨架屏,继续用无骨架屏应用的白屏检测方式已经无法判断白屏,因为骨架屏也是有效的 dom 元素。

ts 复制代码
import { SDKConfig } from "./types";
import { Logger } from "./Logger";

export class SkeletonScreenMonitor {
  private config: SDKConfig;
  private onSkeletonDisappear: () => void;
  private onWhiteScreenDetected: () => void; // 新增:白屏回调
  private logger: Logger;
  private isSkeletonScreenGone: boolean = false; // 确保回调只执行一次
  private observer: MutationObserver | null = null; // 保存 MutationObserver 实例
  private timeoutId: number | null = null; // 保存定时器 ID

  constructor(
    config: SDKConfig,
    onSkeletonDisappear: () => void,
    onWhiteScreenDetected: () => void,
    logger: Logger
  ) {
    this.config = config;
    this.onSkeletonDisappear = onSkeletonDisappear;
    this.onWhiteScreenDetected = onWhiteScreenDetected; // 初始化白屏回调
    this.logger = logger;
  }

  // 监听骨架屏的消失并开始白屏检测
  waitForSkeletonToDisappear(): void {
    const skeleton = document.querySelector(this.config.skeletonSelector);

    if (!skeleton) {
      this.logger.info("No skeleton screen detected");
      this.triggerSkeletonDisappear(); // 没有检测到骨架屏时,直接调用回调

      return;
    }

    this.observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.removedNodes.forEach((removedNode) => {
          if (
            removedNode instanceof Element &&
            removedNode.matches(this.config.skeletonSelector)
          ) {
            this.logger.info("Skeleton screen disappeared");
            this.disconnectObserver(); // 骨架屏消失时断开 observer
            this.triggerSkeletonDisappear(); // 骨架屏消失时调用回调
            this.checkForWhiteScreen(); // 骨架屏消失后,检查白屏
          }
        });
      });
    });

    this.observer.observe(document.body, { childList: true, subtree: true });

    // 设置超时机制,如果骨架屏在最大等待时间内未消失,强制触发回调
    this.timeoutId = window.setTimeout(() => {
      this.disconnectObserver();

      if (!this.isSkeletonScreenGone) {
        this.logger.warn("Skeleton screen wait timeout");
        this.triggerSkeletonDisappear();
      }

      this.checkForWhiteScreen(); // 超时后检查白屏
    }, this.config.skeletonMaxWaitTime);
  }

  // 用于触发骨架屏消失的回调,并确保只触发一次
  private triggerSkeletonDisappear(): void {
    if (!this.isSkeletonScreenGone) {
      this.isSkeletonScreenGone = true;
      this.onSkeletonDisappear(); // 确保回调只执行一次
    }
  }

  // 检查页面是否有有效的内容,判断是否为白屏
  private checkForWhiteScreen(): void {
    const visibleContent = this.getVisibleContent();

    if (visibleContent === 0) {
      this.logger.error("White screen detected");
      this.onWhiteScreenDetected(); // 白屏检测回调
    } else {
      this.logger.info(`Visible content detected: ${visibleContent} elements`);
    }
  }

  // 获取页面中可见的元素数目
  private getVisibleContent(): number {
    const visibleElements = document.querySelectorAll(
      this.config.contentSelector
    ); // 获取父容器的内容
    let visibleCount = 0;

    // 遍历父容器下的所有子元素,检查它们的显示状态
    visibleElements.forEach((element) => {
      // 检查该元素是否可见
      const rect = element.getBoundingClientRect();
      const computedStyle = window.getComputedStyle(element);

      // 只有当元素有尺寸且不是被隐藏时,才算作可见
      if (
        rect.width > 0 &&
        rect.height > 0 &&
        computedStyle.visibility !== "hidden" &&
        computedStyle.display !== "none"
      ) {
        visibleCount++;
      }
    });

    return visibleCount;
  }

  // 断开 observer 并清除定时器
  disconnectObserver(): void {
    if (this.observer) {
      this.observer.disconnect();
      this.logger.info("Disconnected skeleton screen observer");
      this.observer = null;
    }

    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
      this.logger.info("Cleared skeleton screen timeout");
      this.timeoutId = null;
    }
  }
}

在 SkeletonScreenMonitor 类中,白屏检测的核心在于监听骨架屏的消失,并在合适的时机判断页面是否渲染出有效内容。骨架屏作为页面加载过程中的占位符,当它被移除时,就意味着页面的真实内容应该已经加载完成,因此白屏检测会在这个时机进行。检测方法 checkForWhiteScreen 会在骨架屏消失后被调用,检查页面中是否存在可见的内容。如果没有可见内容,则会触发 onWhiteScreenDetected 回调,认为页面出现白屏。

具体的检测方式是 getVisibleContent 方法,它通过遍历页面中的元素,检查它们是否具有非零尺寸,并且 visibility 不是 hidden,display 不是 none,从而判断页面是否真正渲染了内容。如果页面中没有符合这些条件的可见元素,就认为是白屏,并触发相应的回调。

此外,为了防止某些情况下骨架屏长时间未消失,代码还设置了超时机制。如果骨架屏在设定的最大等待时间内没有移除,则强制触发 onWhiteScreenDetected,确保白屏检测不会因为骨架屏未消失而被跳过。为了准确捕捉骨架屏的消失,MutationObserver 监听 document.body 的变化,一旦骨架屏被移除,就立即执行白屏检测,确保页面已经正常渲染。即使骨架屏移除后,仍然需要确认页面的主要内容是否真正加载,防止误判。

参考资料

总结

前端白屏是指用户访问网页时,页面未能正常加载或渲染,导致浏览器显示空白。这通常由资源加载失败、代码执行错误或网络问题引起。白屏问题在单页面应用(SPA)中尤为复杂,可能导致用户无法看到任何有效内容。

白屏检测可以通过多种方案实现。常见的方法包括监听根节点的事件、使用 MutationObserver 监听 DOM 变化、页面截图对比、前端错误监测以及页面元素对比。每种方案在准确性、开发难度和适用范围上有所不同。基于采样点的检测方案,通过选取页面上的多个采样点,检查是否有有效内容来判断页面是否为白屏。检测时需要平衡性能开销和检测精度,常见的采样方式有垂直采样、交叉采样和垂直交叉采样。

为了确保检测结果的准确性,白屏检测工具通常会结合页面加载状态、骨架屏的消失时机以及页面内容的渲染状态来判断是否为白屏。在微前端和 iframe 场景中,需要特别注意子应用的白屏检测。通过完善白屏检测,能够在早期发现问题,减少重大故障发生的几率,提升用户体验。

相关推荐
I will.874几秒前
如何使用 CSS 实现黑色遮罩效果
前端·javascript·css
守城小轩17 分钟前
Chrome 扩展开发 API实战:Bookmarks(二)
前端·javascript·chrome
gqkmiss22 分钟前
Chrome 浏览器 133 版本新特性
前端·chrome·浏览器·chrome 浏览器
A阳俊yi31 分钟前
SpringMVC中有关请求参数的问题(映射路径,传递不同的参数)
java·前端·javascript
鱼樱前端1 小时前
Vue3 + TypeScript + Better-Scroll 极简上拉下拉组件
前端·javascript·vue.js
Trae首席推荐官1 小时前
Trae 功能上新:支持 Remote-SSH 和自定义模型配置
前端·后端·trae
影子信息1 小时前
element tree树形结构默认展开全部
前端·javascript·vue.js
Riesenzahn1 小时前
说说你对CSS中@layer的了解
前端·javascript
甜点cc1 小时前
前端每个组件外面套一层el-form,这样好吗?
前端·javascript·vue.js