面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:
yunmz777
。
前端白屏是指用户打开网页时,页面未能正常加载或渲染,导致浏览器显示一片空白。这通常是由于 JavaScript 错误、资源加载失败、网络问题或渲染逻辑错误引起的。尤其在单页面应用(SPA)中,前端白屏问题变得更加复杂,可能导致用户无法看到任何有效内容。解决白屏问题需要快速定位并修复错误,确保资源正确加载和渲染。
白屏的表现和原因
白屏的表现通常表现为以下几种情况:
-
页面空白:用户打开页面时,只能看到一个空白的浏览器窗口,没有任何内容显示。页面完全没有渲染,背景通常是白色的。
-
加载中状态无变化:页面在加载过程中,可能显示一个加载动画或进度条,但这个动画或进度条会停滞不前,长时间没有进展,最终变成空白。
-
部分元素未显示:有时页面的某些内容未能渲染出来,可能只有一部分元素(如背景或某些框架)显示,其他部分完全消失,形成一个不完整的页面。
-
浏览器错误提示:在开发环境下,开发者可能看到浏览器的控制台报错信息(如 JavaScript 错误、资源加载失败等),但用户端则仅显示空白页面。
-
不响应交互操作:页面空白且无法进行任何交互。用户尝试点击或滚动页面,但页面没有任何反应。
-
无状态或黑屏:不是完全的白色屏幕,而是页面没有任何内容,背景可能变为黑色或灰色,但这通常表示页面渲染异常。
导致白屏的原因分为两种:资源加载错误、代码执行错误。
资源加载错误
资源加载错误通常指页面依赖的静态文件或网络请求未能成功加载,导致页面无法显示内容。常见的资源加载错误有:
-
JavaScript 文件加载失败:如果必需的 JavaScript 文件未能加载(可能是文件路径错误、网络问题、服务器故障等),页面中的动态功能无法执行,导致空白。
-
CSS 文件加载失败:如果页面样式表加载失败,页面元素的布局和样式无法应用,可能导致页面看起来是空白的。
-
图片、字体等静态资源加载失败:如果页面依赖的图片、字体等静态资源加载失败,可能导致页面内容不完整,甚至出现空白。
-
API 请求失败:如果页面需要通过 API 获取数据,而 API 请求失败或返回错误(如 404 或 500 错误),可能导致页面的动态内容无法渲染。
代码执行错误
代码执行错误是指 JavaScript 脚本在浏览器中运行时出现问题,导致页面的渲染过程被中断。常见的代码执行错误有:
-
JavaScript 运行时错误:在页面的脚本执行过程中,可能会出现未捕获的异常(例如访问未定义的变量、调用不存在的函数等),这些错误会阻止后续代码的执行,导致页面无法渲染。
-
框架渲染错误:在使用像 React、Vue 或 Angular 等前端框架时,如果框架的组件渲染出现错误(如组件数据异常、状态管理错误等),也可能导致页面白屏。
-
异步代码问题:前端代码中大量依赖异步操作(例如使用 fetch 或 axios 发起 API 请求),如果这些异步操作失败(如网络问题、服务器返回错误等),也可能导致页面渲染失败。
-
无限循环或死锁:前端代码中的逻辑错误(如无限递归、死循环等)会使 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);
这几种前端白屏检测方案的主要区别在于检测原理和适用场景:
-
检测节点是否挂载 主要通过监听页面根节点的事件来判断页面是否加载成功,适用于简单的 SPA 框架,但通用性差。
-
监听 DOM 变化 利用
MutationObserver
API 动态监控 DOM 变化,适用于动态渲染,但准确度较低,无法检测未渲染的内容。 -
页面截图对比 通过对比当前页面截图与预期截图来判断页面是否正确渲染,适用于检测页面的完整性,但无法处理动态内容。
-
前端错误监测 通过捕获 JavaScript 错误来检测页面崩溃,适用于框架中的错误检测,但无法捕获页面显示异常。
-
页面元素对比 利用
elementsFromPoint
API 获取页面元素信息,检查页面显示,精确度高,但开发成本较高,适合复杂页面。
每种方法在精确度、开发难度和适用范围上有所不同。
通过以上对比发现,采用 页面关键点采样对比
的实现方案较好。
需要注意的是,对于主应用内嵌入的 iframe 的场景,因为每次采样取到的都是整个 iframe 元素,所以无法在主应用侧判断 iframe 是否白屏,需要在 iframe 应用内接入白屏检测 SDK。
数据采集
这个流程图详细描述了前端白屏检测的步骤,尤其考虑到骨架屏应用的特殊情况。首先,系统会判断页面是否是骨架屏应用。这是因为骨架屏是一种页面加载过程中的占位符,通常会显示一个简单的框架或动画,以给用户提示页面正在加载。骨架屏本身可能会展示一个白屏状态,因此需要额外判断,确保不是因为骨架屏造成的误判。
如果页面是骨架屏应用,接着会检查页面是否加载完成或出现加载错误。如果页面加载完成且没有错误,系统会在浏览器空闲时进行屏幕采样,判断当前页面是否满足白屏标准。如果符合标准,表示页面可能处于白屏状态,系统进一步判断是否开启了轮询检测。如果未开启,系统将启动轮询检测;如果已经开启,系统会比较初次采样结果与后续结果是否一致,确保页面没有发生变化。如果结果不一致,说明页面可能出现了白屏,系统将上报错误。
整个流程通过这些判断步骤,确保能够正确识别和上报白屏问题,尤其是在骨架屏这种特殊情况下,避免误判。
屏幕采样点选取
采样点的选取有三种方式:垂直采样、交叉采样、垂直交叉采样。
垂直采样
垂直采样是指从页面的顶部到底部沿垂直方向进行采样,逐行或以一定间隔采样页面的内容。这种方法可以简单而有效地检测页面的渲染情况,特别适用于页面内容有明显的垂直布局(如文章、博客等)。
但是如果页面的内容横向布局或者大量动态加载的部分,垂直采样可能无法完全覆盖页面的渲染状态。
交叉采样
交叉采样是指从页面的多个位置同时进行采样,既涵盖垂直方向,也涉及水平方向的多个位置。通常,它通过交替选择页面不同区域的采样点来进行检测。交叉采样能够捕捉更多页面细节,特别适用于多列布局或复杂页面结构(如新闻网站、电子商务平台等),可以更全面地反映页面的渲染情况。与垂直采样相比,交叉采样需要更多的计算资源,采样点较多,因此在实现上更复杂,可能会带来额外的性能开销。
垂直交叉采样
垂直交叉采样结合了垂直采样和交叉采样的特点,既沿页面的垂直方向从顶部到底部进行采样,又在不同的垂直线间交替进行水平采样。这样可以覆盖页面更广泛的区域,既能检查整体布局,又能捕捉细节,适用于复杂的页面结构。由于采样点增多,计算量较大,可能对性能造成一定压力,因此在实际应用中需要平衡检测精度与性能开销。
为了克服垂直交叉采样带来的性能问题,我们可以利用 requestIdleCallback 在浏览器空闲时执行计算。由于 requestIdleCallback 只在空闲时段运行,它不会阻塞页面渲染或影响用户操作的响应速度,从而有效减轻了计算负担。
白屏的判断标准和检测时机
有骨架屏和无骨架屏应用的检测方式不一样,检测时机也有细微差别。
无骨架屏场景
确定可见元素的检测时机是一个关键挑战,时机把握不当容易导致误判。如果检测过早,可能会将页面加载过程中的短暂空白误认为是白屏,从而无法准确反映页面最终的渲染状态;而如果检测过晚,当用户因长时间等待而关闭页面时,白屏情况就会错过,导致漏测。因此,合理选择检测时机至关重要,既要避免过早判断,也要防止延迟导致漏报。
检测时机
检测的时机主要分为以下几个方面:
-
页面加载完成后:在页面加载完成并没有出现任何加载错误时,监控会开始进行初始的采样。startMonitoring 方法作为监控的入口,会在方法中调用 requestIdleCallback 来进行浏览器空闲时的采样。
-
浏览器空闲时采样:通过使用 requestIdleCallback 来执行采样任务,确保采样操作不会阻塞页面渲染和用户交互。requestIdleCallback 确保在浏览器空闲时执行,从而提升性能并减少对用户体验的影响。
-
页面内容变化检测:在每次采样时,系统会将当前的采样数据与初始采样数据进行对比,检查页面是否发生变化。如果页面发生了变化,系统会进行异常上报(reportAnomaly 方法),并启动重试机制(retrySampling 方法)。
-
白屏检测:系统每次采样时都会判断页面中是否有空白区域。若空白区域超过设定的阈值(threshold),则认为页面为白屏并触发上报。白屏检测基于页面元素的有无和可视状态进行判定,通过大量的采样点来增强准确性。
-
重试机制:在页面发生变化或者检测到白屏的情况下,系统会启动重试机制。通过 retrySampling 方法,系统会根据设定的最大重试次数和重试间隔进行多次采样,直到检测到页面发生变化或达到最大重试次数。
-
停止监控:在监控过程中,如果页面发生特殊情况(例如,页面请求被取消,或者页面被强制停止),可以调用 stopMonitoring 方法来停止监控。调用时,requestIdleCallback 会停止采样,监控任务也会被清除,避免不必要的资源消耗。
检测方式
无骨架屏的检测方式主要有以下几个方面:
-
初始化和配置根容器:在初始化 SDK 时,我们需要根据页面的结构配置根容器。如果根容器为空(即页面中没有有效的容器元素),则认为页面发生了白屏。在代码中,根容器元素的判断通过 isContainer 方法实现。该方法通过判断元素的 tagName 是否为常见容器元素(如 DIV, SECTION, MAIN, HEADER, FOOTER)来确定是否是有效的根容器。
-
采样点的计算和检测:通过获取页面的宽度 (window.innerWidth) 和高度 (window.innerHeight),代码计算了多个采样点的坐标,涵盖了页面的各个方向(包括 X 轴、Y 轴、上升对角线和下降对角线)。使用 document.elementsFromPoint 方法获取每个坐标的 DOM 元素,并判断这些元素是否为有效的根容器元素。若某些采样点没有有效的容器元素,便增加空点计数。
-
白屏检测:每次采样后,系统会检查页面中是否有空白区域。空白区域超过设定的阈值(threshold)时,认为页面发生了白屏并进行上报。为了避免遗漏微前端或 iframe 场景的子应用,代码选定页面的 右下方 作为内容区,通过检查该区域内的采样点来判断是否满足白屏条件。这样可以确保即使主应用中没有白屏,子应用的白屏问题也能够被检测到。
-
重试机制:在检测到页面内容发生变化或白屏时,系统会启动重试机制。通过 retrySampling 方法,系统会在设定的最大重试次数和重试间隔内进行多次采样,直到页面状态发生变化或达到最大重试次数。此机制确保了即使在页面动态变化时,监控也能够及时捕捉到状态变化。
-
停止监控:当页面发生特殊情况(如请求被取消或页面被强制停止)时,监控会被停止。通过 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++;
}
}
骨架屏场景
检测的时机主要分为以下几个方面:
-
监听骨架屏消失:使用 MutationObserver 监听 DOM 变化,当骨架屏从页面中移除时,触发白屏检测。
-
超时机制:如果骨架屏在 skeletonMaxWaitTime 内没有消失,强制触发白屏检测,防止页面无限等待。
-
骨架屏消失后检测白屏:当骨架屏移除后,立即检查页面是否有有效内容,确保页面正确渲染。
检测方式
如果应用内有骨架屏,继续用无骨架屏应用的白屏检测方式已经无法判断白屏,因为骨架屏也是有效的 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 场景中,需要特别注意子应用的白屏检测。通过完善白屏检测,能够在早期发现问题,减少重大故障发生的几率,提升用户体验。