前言
前端基建里最重要的事情之一就是监控,性能,报错,白屏等等,而今天要说的就是白屏的监控。
前端白屏是影响用户体验的常见问题,通常有资源加载失败、JS 执行错误、渲染阻塞、框架异常等原因。
今天就以页面生命周期、错误捕获、性能指标、框架特性等维度来描述怎么监控。
关键节点判断
核心原理
不管是传统框架、界面、还是现代浏览器框架,都会有一个容器节点、关键节点,例如根节点,header节点,logo 节点等等,我们要做的就是在页面加载完成之后判断它是否存在即可
关键检测维度
- 元素是否存在:
document.querySelector(selector)是否返回非 null 值(排除因 HTML 结构错误导致的元素缺失)。 - 是否有实际内容:元素的
textContent.trim()不为空(排除空标签),或childNodes.length > 0(存在子元素)。 - 是否可见:
- 布局可见性:
offsetHeight > 0且offsetWidth > 0(排除display: none或内容被完全遮挡)。 - 样式可见性:
getComputedStyle(element).visibility !== 'hidden'且opacity > 0(排除透明或隐藏样式)。
- 布局可见性:
function checkCriticalElement(selector, options = {}) {const { timeout = 5000, // 超时阈值(默认5秒)interval = 500, // 检测间隔(默认500ms,平衡精度与性能)onWhiteScreen = () => {} // 白屏回调} = options;const startTime = Date.now();const timer = setInterval(() => {const now = Date.now();// 1. 超时判断:超过阈值仍未检测到有效元素,触发白屏if (now - startTime > timeout) {clearInterval(timer);onWhiteScreen({type: 'critical_element_timeout',selector,duration: now - startTime,reason: '元素未在规定时间内加载完成'});return;}// 2. 元素存在性检测const element = document.querySelector(selector);if (!element) return; // 元素未加载,继续等待// 3. 内容有效性检测const hasContent = element.textContent.trim() !== '' || element.childNodes.length > 0;if (!hasContent) return; // 元素存在但无内容,继续等待// 4. 可见性检测const computedStyle = getComputedStyle(element);const isVisible = element.offsetHeight > 0 && element.offsetWidth > 0 && computedStyle.visibility !== 'hidden' && computedStyle.opacity > 0;if (isVisible) {clearInterval(timer); // 所有条件满足,停止检测}}, interval);
}
触发时机
- 首屏加载:在
DOMContentLoaded事件后启动检测 - 单页应用(SPA)路由切换:在路由钩子(如 Vue 的
router.afterEach、React 的useEffect监听路由变化)中触发,检测新页面的关键元素。 - 动态内容加载:对于异步渲染的内容(如列表、表单),在接口请求完成后启动检测。
错误捕获
1. JS 运行时错误捕获
同步错误(window.onerror)
- 触发场景:直接执行的 JS 代码抛出未捕获的错误(如
undefined.xxx、语法错误)。 - 参数详解:
message:错误信息(字符串)。source:错误发生的脚本 URL。lineno/colno:错误行号 / 列号。error:错误对象(含stack调用栈,最关键的排查依据)。
window.onerror = function(message, source, lineno, colno, error) {// 过滤非关键错误(如第三方脚本的非阻塞错误)const isCritical = source.includes('/app.') || source.includes('/main.'); // 仅关注核心脚本if (isCritical) {reportError({type: 'js_runtime_error',message: error?.message || message,stack: error?.stack || `at ${source}:${lineno}:${colno}`,time: Date.now()});}return true;
};
异步错误(window.onunhandledrejection)
- 触发场景:Promise 链式调用中未通过
.catch()处理的错误(如接口请求失败、async/await未用try/catch)。
window.onunhandledrejection = function(event) {const reason = event.reason;reportError({type: 'unhandled_promise',message: reason?.message || String(reason),stack: reason?.stack,time: Date.now()});event.preventDefault(); // 阻止浏览器默认警告
};
2. 资源加载错误捕获
触发场景
- 脚本(
<script>)加载失败(404/500 状态、跨域限制)。 - 样式表(
<link rel="stylesheet">)加载失败(导致页面无样式,视觉上白屏)。
window.addEventListener('error', (event) => {const target = event.target;// 仅处理资源加载错误if (!['SCRIPT', 'LINK', 'IMG'].includes(target.tagName)) return;// 判断是否为关键资源(根据业务定义)const isCritical = (target.tagName === 'SCRIPT' && target.src.includes('/vue.runtime') || target.src.includes('/app.')) ||(target.tagName === 'LINK' && target.rel === 'stylesheet' && target.href.includes('/main.css'));if (isCritical) {reportError({type: 'resource_load_error',tag: target.tagName,url: target.src || target.href,status: target.error?.status || 'unknown', // 部分浏览器返回HTTP状态码time: Date.now()});}
}, true); // 捕获阶段监听
小结
快速定位因代码错误或资源缺失导致的白屏(如框架脚本加载失败直接导致无法渲染)。但是并非所有错误都会导致白屏(如非首屏脚本错误),需通过 “关键资源 / 脚本” 过滤。
基于性能指标的检测
核心原理
Web 性能 API 提供了页面加载和渲染的关键时间节点,通过监控这些指标可判断渲染是否正常:
- 若 “首屏绘制(FCP)” 未发生或超时,说明页面未开始渲染;
- 若 “最大内容绘制(LCP)” 超时,说明核心内容未加载完成,可能处于白屏或半成品状态。
1. 首屏绘制(FCP)监控
定义
FCP(First Contentful Paint)指浏览器首次绘制文本、图片、非白色背景的 SVG 或 Canvas 元素的时间,是页面 “从白屏到有内容” 的第一个关键节点。
检测逻辑
- 通过
PerformanceObserver监听first-contentful-paint类型的性能条目。 - 若 FCP 时间超过业务阈值(如 8 秒),或未检测到 FCP 条目(说明未开始渲染),则判定为白屏风险。
// 监听FCP指标
const fcpObserver = new PerformanceObserver((entriesList) => {const entries = entriesList.getEntries();if (entries.length === 0) return;const fcpEntry = entries[0];const fcpTime = fcpEntry.startTime; // 相对于页面导航开始的时间(ms)const navigationStart = performance.timing.navigationStart;const absoluteTime = new Date(navigationStart + fcpTime).toISOString(); // 绝对时间// 阈值判断(根据业务场景调整,如低端设备可放宽至10秒)if (fcpTime > 8000) {reportPerformance({type: 'fcp_timeout',fcpTime: Math.round(fcpTime),absoluteTime,message: `首屏绘制超时(阈值8秒)`});}
});// 启动监听(buffered: true 表示监听已发生的指标)
fcpObserver.observe({ type: 'first-contentful-paint', buffered: true });// 兜底:若页面加载完成后仍未检测到FCP,判定为白屏
window.addEventListener('load', () => {const fcpEntries = performance.getEntriesByType('first-contentful-paint');if (fcpEntries.length === 0) {reportPerformance({ type: 'fcp_missing', message: '未检测到首屏绘制' });}
});
2. 最大内容绘制(LCP)监控
定义
LCP(Largest Contentful Paint)指页面加载过程中,最大的内容元素(文本块或图片)完成绘制的时间,反映核心内容的加载进度。
检测逻辑
- LCP 通常在 FCP 之后发生,若 LCP 超时(如 12 秒),说明核心内容未加载,可能处于 “部分白屏” 状态。
- 记录 LCP 对应的元素(
fcpEntry.element),便于分析是文本还是图片未加载。
const lcpObserver = new PerformanceObserver((entriesList) => {const entries = entriesList.getEntries();if (entries.length === 0) return;// LCP可能会多次触发(如图片加载完成后尺寸变化),取最后一次const lcpEntry = entries[entries.length - 1];const lcpTime = lcpEntry.startTime;if (lcpTime > 12000) { // 阈值12秒reportPerformance({type: 'lcp_timeout',lcpTime: Math.round(lcpTime),element: lcpEntry.element?.outerHTML || 'unknown', // 记录最大内容元素message: `最大内容绘制超时(阈值12秒)`});}
});lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
3. 页面加载阶段耗时分析
通过 performance.timing 分析各阶段耗时,定位阻塞渲染的环节:
domInteractive:DOM 结构解析完成时间(若过长,可能是 HTML 体积过大或解析阻塞)。domContentLoadedEventEnd:DOM 解析 + 初始脚本执行完成时间(若过长,可能是同步脚本执行耗时)。loadEventEnd:所有资源(图片、样式等)加载完成时间(若过长,可能是资源过多或网络慢)。
window.addEventListener('load', () => {const timing = performance.timing;const navigationStart = timing.navigationStart;// 计算各阶段耗时const domParseTime = timing.domInteractive - navigationStart; // DOM解析耗时const scriptExecTime = timing.domContentLoadedEventEnd - timing.domInteractive; // 初始脚本执行耗时const resourceLoadTime = timing.loadEventEnd - timing.domContentLoadedEventEnd; // 资源加载耗时// 异常判断if (domParseTime > 3000) { // DOM解析超过3秒reportPerformance({ type: 'dom_parse_slow', domParseTime });}if (scriptExecTime > 5000) { // 脚本执行超过5秒(可能阻塞渲染)reportPerformance({ type: 'script_exec_slow', scriptExecTime });}
});
小结
- 适用于检测因 “渲染阻塞”(如慢脚本、大资源)导致的白屏,尤其适合首屏加载场景。
- 性能指标受设备和网络影响极大(如 3G 网络 FCP 阈值应高于 WiFi),需结合用户设备等级动态调整
框架钩子监听
核心原理
单页应用(SPA)的渲染逻辑依赖框架(Vue/React)的组件系统,框架层面的异常(如组件渲染失败、路由跳转错误)是白屏的高频原因。框架提供了专属的错误捕获机制,可精准定位组件级问题。
React 框架异常监听
ErrorBoundary 组件
- 原理:React 16+ 提供的错误边界机制,可捕获子组件树中的渲染错误、生命周期错误、构造函数错误,并返回降级 UI(避免整个应用崩溃白屏)。
- 限制:无法捕获以下错误:
- 事件处理函数中的错误(需手动
try/catch); - 异步代码中的错误(如
setTimeout、Promise); - 服务器端渲染错误;
- 自身组件的错误(仅捕获子组件)。
- 事件处理函数中的错误(需手动
class ErrorBoundary extends React.Component {constructor(props) {super(props);this.state = { hasError: false, error: null, errorInfo: null };}// 静态方法:更新状态以触发降级UIstatic getDerivedStateFromError(error) {return { hasError: true, error };}// 实例方法:捕获错误并上报componentDidCatch(error, errorInfo) {this.setState({ errorInfo });reportFrameworkError({framework: 'react',type: 'component_error',message: error.message,stack: error.stack,componentStack: errorInfo.componentStack, // React组件调用栈route: window.location.pathname // 当前路由});}render() {if (this.state.hasError) {// 降级UI:避免白屏,提示用户刷新return (<div style={{ padding: '20px', textAlign: 'center' }}><h2>页面加载出错了</h2><button onClick={() => window.location.reload()}>刷新重试</button></div>);}return this.props.children;}
}// 使用方式:包裹整个应用或关键路由
ReactDOM.render(<ErrorBoundary><BrowserRouter><App /></BrowserRouter></ErrorBoundary>,document.getElementById('root')
);
路由错误监听(React Router)
- 异步路由加载失败(如
React.lazy+Suspense加载组件失败)可通过 ErrorBoundary 捕获,或在loadable等库中监听错误。
小结
适用于SPA 应用中因组件渲染、路由跳转导致的白屏(占 SPA 白屏问题的 60% 以上)
像素检测
核心原理
部分白屏场景无错误日志且关键元素存在(如 CSS 样式错乱导致内容被隐藏、背景色与内容色一致),此时需从视觉像素层面判断是否有有效内容。
实现方案(两种思路)
1. 简化版:基于元素尺寸与内容密度
通过检测页面核心区域的尺寸和内容复杂度判断,避免高性能消耗的像素分析:
- 核心区域(如
#app)的scrollHeight是否大于视口高度(排除完全空白)。 - 内容密度:文本长度 + 图片数量是否达到阈值(如文本 > 100 字符或图片 > 1 张)。
function checkVisualContentDensity() {const app = document.querySelector('#app');if (!app) return false;// 1. 尺寸检测:核心区域高度是否足够(至少为视口的80%)const viewportHeight = window.innerHeight;const appHeight = app.scrollHeight;if (appHeight < viewportHeight * 0.8) return false;// 2. 内容密度检测:文本长度 + 图片数量const textLength = app.textContent.trim().length;const imageCount = app.querySelectorAll('img[src]').length;const hasEnoughContent = textLength > 100 || imageCount > 0;return hasEnoughContent;
}// 定时检测(如路由切换后3秒)
setTimeout(() => {if (!checkVisualContentDensity()) {reportVisualError({type: 'low_content_density',message: '页面内容密度过低,可能存在视觉白屏'});}
}, 3000);
2. 进阶版:基于 Canvas 像素分析
通过 html2canvas 库将页面关键区域转为 Canvas,分析像素颜色分布:
- 若超过 90% 的像素为同一颜色(如白色
#ffffff),判定为白屏。
import html2canvas from 'html2canvas';async function checkVisualPixels() {const app = document.querySelector('#app');if (!app) return;try {// 将#app区域转为Canvasconst canvas = await html2canvas(app, {useCORS: true, // 允许跨域图片logging: false});const ctx = canvas.getContext('2d');const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const pixels = imageData.data; // 像素数据(RGBA数组)// 统计白色像素占比(RGB均为255,透明度255)let whitePixelCount = 0;const totalPixels = pixels.length / 4; // 每个像素4个值(RGBA)for (let i = 0; i < pixels.length; i += 4) {const r = pixels[i];const g = pixels[i + 1];const b = pixels[i + 2];const a = pixels[i + 3];if (r === 255 && g === 255 && b === 255 && a === 255) {whitePixelCount++;}}const whiteRatio = whitePixelCount / totalPixels;if (whiteRatio > 0.9) { // 白色像素占比超90%reportVisualError({type: 'high_white_ratio',ratio: whiteRatio.toFixed(2),message: `页面白色像素占比过高(${whiteRatio*100}%)`});}} catch (err) {console.error('像素分析失败', err);}
}// 谨慎使用:性能消耗较高,建议仅在关键场景触发(如其他检测疑似白屏时)
checkVisualPixels();
小结
- 性能消耗大(Canvas 绘制和像素分析耗时),不宜高频执行。
- 受页面设计影响(如本身为极简风格,白色占比高易误报)。
- 本人不太推荐只使用此种方案。
总结
在生产环境中,前端白屏监听的核心目标是:高覆盖率(覆盖绝大多数白屏场景)、低误报(避免无效告警)、低性能损耗(不影响用户体验)、可溯源(能定位根因)。
单一方法难以覆盖所有白屏场景,需结合多种手段形成闭环:
| 方法类型 | 核心手段 | 适用场景 |
|---|---|---|
| 关键元素检测 | 定时检查 DOM 存在性和内容 | 首屏加载、路由切换后白屏 |
| 错误捕获 | JS 错误、资源加载错误 | 代码异常导致的白屏 |
| 性能指标监控 | FCP、LCP、DOM 就绪时间 | 渲染阻塞导致的白屏 |
| 框架异常监听 | Vue errorHandler、React ErrorBoundary | 组件渲染错误导致的白屏 |
| 视觉检测 | 内容高度 / 像素分析 | 样式错乱导致的白屏 |
个人结尾推荐先使用JS 错误捕获 + 资源加载错误捕获+框架错误捕获, 作为最核心的错误监控,其余的检测方式可根据具体场景再行分析。