白屏时间监控

计算和监控页面白屏时间的几种方案

问题

如何计算页面的白屏时间?有哪些监控方案?

解答

白屏时间指从用户输入 URL 到页面开始显示内容的时间,是衡量用户体验的重要指标。

方案一:Performance API

使用浏览器原生的 Performance API 获取 FP(First Paint)或 FCP(First Contentful Paint)。

// 获取白屏时间
function getWhiteScreenTime() {
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries()
    
    entries.forEach((entry) => {
      if (entry.name === 'first-paint') {
        console.log('FP(首次绘制):', entry.startTime, 'ms')
      }
      if (entry.name === 'first-contentful-paint') {
        console.log('FCP(首次内容绘制):', entry.startTime, 'ms')
      }
    })
  })
  
  // 监听 paint 类型的性能条目
  observer.observe({ entryTypes: ['paint'] })
}

// 页面加载后获取
getWhiteScreenTime()

// 也可以直接从 performance 中获取(需要在页面加载完成后)
function getWhiteScreenTimeSync() {
  const paintEntries = performance.getEntriesByType('paint')
  
  const fp = paintEntries.find((entry) => entry.name === 'first-paint')
  const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')
  
  return {
    fp: fp ? fp.startTime : null,
    fcp: fcp ? fcp.startTime : null
  }
}

方案二:手动打点

<head> 标签末尾打点,计算从 navigationStart 到打点时刻的时间。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>白屏时间监控</title>
  <link rel="stylesheet" href="style.css">
  <script src="blocking-script.js"></script>
  
  <!-- 在 head 末尾打点 -->
  <script>
    window.whiteScreenTime = Date.now() - performance.timing.navigationStart
    // 或者使用高精度时间
    // window.whiteScreenTime = performance.now()
  </script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

方案三:MutationObserver 监控 DOM 变化

监控 DOM 变化,当页面有实际内容渲染时记录时间。

function observeWhiteScreen() {
  const startTime = performance.now()
  let hasContent = false
  
  const observer = new MutationObserver((mutations) => {
    // 检查是否有可见内容
    if (!hasContent && document.body) {
      const hasVisibleContent = checkVisibleContent()
      
      if (hasVisibleContent) {
        hasContent = true
        const whiteScreenTime = performance.now() - startTime
        console.log('白屏时间:', whiteScreenTime, 'ms')
        observer.disconnect()
      }
    }
  })
  
  observer.observe(document, {
    childList: true,
    subtree: true
  })
}

// 检查页面是否有可见内容
function checkVisibleContent() {
  const elements = document.body.querySelectorAll('*')
  
  for (const el of elements) {
    const rect = el.getBoundingClientRect()
    const style = getComputedStyle(el)
    
    // 元素可见且在视口内
    if (
      rect.width > 0 &&
      rect.height > 0 &&
      style.visibility !== 'zzinb' &&
      style.display !== 'none' &&
      rect.top < window.innerHeight
    ) {
      return true
    }
  }
  
  return false
}

// 尽早执行
observeWhiteScreen()

完整监控方案

class WhiteScreenMonitor {
  constructor() {
    this.metrics = {}
    this.init()
  }
  
  init() {
    // 方案一:Performance API
    this.observePaint()
    
    // 方案二:页面加载完成后获取完整数据
    window.addEventListener('load', () => {
      this.collectMetrics()
    })
  }
  
  observePaint() {
    if (!window.PerformanceObserver) {
      return
    }
    
    try {
      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          this.metrics[entry.name] = entry.startTime
        })
      })
      
      observer.observe({ entryTypes: ['paint'] })
    } catch (e) {
      console.warn('PerformanceObserver not supported')
    }
  }
  
  collectMetrics() {
    const timing = performance.timing
    
    // 各阶段耗时
    this.metrics = {
      ...this.metrics,
      // DNS 查询
      dns: timing.domainLookupEnd - timing.domainLookupStart,
      // TCP 连接
      tcp: timing.connectEnd - timing.connectStart,
      // 请求响应
      request: timing.responseEnd - timing.requestStart,
      // DOM 解析
      domParse: timing.domInteractive - timing.responseEnd,
      // 资源加载
      resources: timing.loadEventStart - timing.domContentLoadedEventEnd,
      // 白屏时间(近似)
      whiteScreen: timing.domLoading - timing.navigationStart,
      // 首屏时间
      firstScreen: timing.domContentLoadedEventEnd - timing.navigationStart
    }
    
    this.report()
  }
  
  report() {
    console.log('性能指标:', this.metrics)
    
    // 上报到监控平台
    // navigator.sendBeacon('/api/metrics', JSON.stringify(this.metrics))
  }
}

// 使用
new WhiteScreenMonitor()

关键点

  • FP vs FCP:FP 是首次绘制任何内容,FCP 是首次绘制文本/图片等有意义内容
  • Performance API 是最准确的方案,但需要浏览器支持
  • 手动打点适合需要精确控制测量点的场景
  • 影响白屏的因素:CSS 阻塞、JS 阻塞、字体加载、大量 DOM 节点
  • 优化方向:内联关键 CSS、异步加载 JS、预加载关键资源、SSR