博客 / 詳情

返回

前端白屏監控原理

前言

前端基建裏最重要的事情之一就是監控,性能,報錯,白屏等等,而今天要説的就是白屏的監控。
前端白屏是影響用户體驗的常見問題,通常有資源加載失敗、JS 執行錯誤、渲染阻塞、框架異常等原因。
今天就以頁面生命週期、錯誤捕獲、性能指標、框架特性等維度來描述怎麼監控。

關鍵節點判斷

核心原理

不管是傳統框架、界面、還是現代瀏覽器框架,都會有一個容器節點、關鍵節點,例如根節點,header節點,logo 節點等等,我們要做的就是在頁面加載完成之後判斷它是否存在即可

關鍵檢測維度

  1. 元素是否存在document.querySelector(selector) 是否返回非 null 值(排除因 HTML 結構錯誤導致的元素缺失)。
  2. 是否有實際內容:元素的 textContent.trim() 不為空(排除空標籤),或 childNodes.length > 0(存在子元素)。
  3. 是否可見

    • 佈局可見性: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);
    • 異步代碼中的錯誤(如 setTimeoutPromise);
    • 服務器端渲染錯誤;
    • 自身組件的錯誤(僅捕獲子組件)。
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 錯誤捕獲 + 資源加載錯誤捕獲+框架錯誤捕獲, 作為最核心的錯誤監控,其餘的檢測方式可根據具體場景再行分析。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.