前言
前端基建裏最重要的事情之一就是監控,性能,報錯,白屏等等,而今天要説的就是白屏的監控。
前端白屏是影響用户體驗的常見問題,通常有資源加載失敗、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 };
}
// 靜態方法:更新狀態以觸發降級UI
static 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區域轉為Canvas
const 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 錯誤捕獲 + 資源加載錯誤捕獲+框架錯誤捕獲, 作為最核心的錯誤監控,其餘的檢測方式可根據具體場景再行分析。