前端性能监控体系

What — 是什么

前端性能监控(Frontend Performance Monitoring)是一套持续采集、分析、可视化和告警页面性能指标的系统工程,涵盖从用户发起请求到页面可交互全链路的性能数据采集与洞察。它是前端工程化体系中不可或缺的一环,将”页面快不快”从主观感受转化为可量化的客观数据。

核心监控维度:

维度说明典型指标
加载指标衡量页面资源加载速度TTFB、FCP、LCP
交互指标衡量用户操作响应速度FID、INP、TBT
视觉指标衡量页面视觉稳定性与呈现速度CLS、SI
自定义指标业务特定的性能度量首屏业务数据可用时间、搜索结果渲染时间

核心概念

概念说明
Core Web VitalsGoogle 定义的核心 Web 指标,当前为 LCP / INP / CLS 三项
Performance API浏览器原生性能数据采集接口,包括 PerformanceObserver、PerformanceEntry 等
PerformanceObserver基于 Observer 模式的性能数据监听接口,可异步获取各类 PerformanceEntry
PerformanceEntry性能数据条目的基类型,子类型包括 Navigation、Resource、Paint、Long Task 等
分位值(Percentile)描述数据分布的统计量,P75 表示 75% 的用户在此值以下
SLO(Service Level Objective)服务等级目标,如”P75 LCP < 2.5s”
Performance Budget性能预算,为各项指标设定上限,CI/CD 中作为门禁条件
Real User Monitoring(RUM)真实用户监控,采集线上用户实际体验数据
Synthetic Monitoring合成监控,通过自动化工具(Lighthouse)模拟访问

性能监控整体架构

┌─────────────────────────────────────────────────────────────────┐
│                        用户浏览器                                │
│  ┌───────────┐  ┌──────────────┐  ┌──────────────┐             │
│  │ Performance│  │ Resource     │  │ Long Task    │             │
│  │ Observer  │  │ Timing API   │  │ API          │             │
│  └─────┬─────┘  └──────┬───────┘  └──────┬───────┘             │
│        │               │                 │                      │
│        └───────────────┼─────────────────┘                      │
│                        ▼                                        │
│               ┌────────────────┐                                │
│               │  采集 SDK      │                                │
│               │  指标计算/组装  │                                │
│               └───────┬────────┘                                │
│                       │ sendBeacon / XHR                        │
└───────────────────────┼────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                       数据服务端                                 │
│  ┌──────────┐   ┌───────────┐   ┌───────────┐   ┌──────────┐  │
│  │ 日志接收  │──▶│ ETL 清洗  │──▶│ 聚合计算   │──▶│ 存储层   │  │
│  │ Gateway  │   │ 维度拆分  │   │ P75/P95   │   │ ClickHouse│  │
│  └──────────┘   └───────────┘   └───────────┘   └──────────┘  │
│                                                      │         │
│  ┌──────────┐   ┌───────────┐   ┌───────────┐        │         │
│  │ 告警服务  │◀──│ 规则引擎   │◀──│ 查询服务   │◀───────┘         │
│  │ 钉钉/邮件 │   │ SLO 检查  │   │ SQL/API   │                  │
│  └──────────┘   └───────────┘   └─────┬─────┘                  │
│                                       │                         │
│  ┌────────────────────────────────────▼──────────────────────┐  │
│  │               可视化看板(Grafana / 自建 Dashboard)        │  │
│  │   趋势图 │ 对比图 │ 分布图 │ 地域分布 │ 浏览器分布          │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Why — 为什么需要

性能直接影响用户体验和业务转化

性能不是技术自嗨,而是实实在在的商业指标驱动因素

  • Google 研究:页面加载时间从 1s 增至 3s,跳出率上升 32%
  • Amazon 数据:每增加 100ms 延迟,销售额下降 1%
  • BBC 实验:页面加载每慢 1s,用户流失增加 10%
  • Pinterest:重构后感知加载时间减少 40%,SEO 流量提升 15%

Google 搜索排名因素

自 2021 年起,Core Web Vitals 已成为 Google 搜索排名的信号。页面体验信号(Page Experience Signals)包含:

  1. Core Web Vitals(LCP / INP / CLS)
  2. 移动端友好性
  3. 安全浏览(HTTPS)
  4. 无插页式广告
  5. HTTPS 安全性

这意味着性能差不仅影响现有用户,还会直接减少新用户的获取渠道

需要数据驱动性能优化

没有监控的优化是盲目的:

  • 不知道慢在哪里:用户反馈”页面卡”,但不知道是加载慢、渲染慢还是交互慢
  • 不知道优化效果:做了优化,但无法量化提升幅度
  • 不知道回归风险:新版本上线后性能劣化,无法及时发现
  • 不知道优先级:多项优化待做,没有数据支撑决策先做哪个

监控方案对比

维度自建监控SentryDataDog阿里云 ARMS
成本人力成本高,服务器成本可控免费版有限额,Team 版 $26/月/用户按量计费,APM Pro $31/月/主机按量计费,基础版 ¥1200/月起
功能完全定制,需自行开发错误监控强,性能监控基础全栈 APM,生态完善阿里云生态集成,前端监控完整
数据安全数据完全自控数据存海外数据存海外数据存国内,符合合规要求
定制性极高中等中等中等
接入成本高(SDK + 服务端 + 看板)低(SDK 接入即可)低(SDK 接入即可)低(SDK 接入即可)
适用团队大型团队,有专职性能团队中小团队,重视错误追踪中大型团队,全栈可观测性国内团队,阿里云生态用户
数据粒度原始数据全量可查聚合数据为主聚合数据为主聚合数据为主,可查明细
实时性自定义,可达秒级分钟级秒级分钟级
告警能力需自建基础告警强大的告警系统集成云监控告警

选型建议:

  • 初创团队:Sentry 免费版起步,覆盖错误监控 + 基础性能
  • 国内中型团队:阿里云 ARMS,合规 + 全链路
  • 全球化团队:DataDog,全栈可观测性
  • 超大型团队:自建监控 + 商业方案补充,极致定制

How — 怎么做

1. Core Web Vitals 指标体系

Core Web Vitals 是 Google 定义的核心 Web 指标集合,代表用户最真实的体验维度。2024 年 3 月,INP 正式替代 FID 成为新的交互指标。

LCP — Largest Contentful Paint(最大内容绘制)

衡量页面主要内容加载速度,即视口中最大的可见内容元素(图片、视频、文本块)的渲染时间。

评分阈值
≤ 2.5s
需改进2.5s ~ 4.0s
> 4.0s

LCP 元素类型优先级:

1. <img> 元素
2. <video> 元素的 poster 图片
3. background-image(url() 加载的图片)
4. <image>(SVG 内)
5. 块级文本元素(<p>、<div>、<article> 等)
// 采集 LCP
function observeLCP() {
  if (!('PerformanceObserver' in window)) return;

  try {
    const po = new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      const lastEntry = entries[entries.length - 1]; // 取最后一次 LCP

      console.log('LCP:', lastEntry.startTime);
      console.log('LCP 元素:', lastEntry.element?.tagName, lastEntry.element?.id);
      console.log('LCP URL:', lastEntry.url); // 图片 URL(如适用)
      console.log('LCP 大小:', lastEntry.size);

      // 上报
      reportMetric({
        name: 'LCP',
        value: lastEntry.startTime,
        rating: lastEntry.startTime <= 2500 ? 'good' :
                lastEntry.startTime <= 4000 ? 'needs-improvement' : 'poor',
        element: lastEntry.element?.tagName,
        url: lastEntry.url,
        size: lastEntry.size,
      });
    });

    po.observe({ type: 'largest-contentful-paint', buffered: true });
  } catch (e) {
    // 浏览器不支持该 entry type
  }
}

LCP 优化方向:

优化方向具体手段
服务端响应CDN、Edge Cache、SSR/SSG、TTFB 优化
资源加载<link rel="preload">fetchpriority="high"、关键 CSS 内联
图片优化WebP/AVIF 格式、srcset 响应式、懒加载非首屏图片
渲染阻塞异步加载非关键 JS/CSS、async/defer
字体优化font-display: swap<link rel="preload"> 字体文件

INP — Interaction to Next Paint(交互到下次绘制)

衡量用户与页面交互后的响应速度,观察用户整个页面生命周期中所有点击、键盘、触摸交互的延迟,取最差(或接近最差)的一次作为 INP 值。2024 年 3 月取代 FID。

评分阈值
≤ 200ms
需改进200ms ~ 500ms
> 500ms
// 采集 INP
function observeINP() {
  if (!('PerformanceObserver' in window)) return;

  let worstINP = 0;
  let allInteractions = [];

  try {
    const po = new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();

      entries.forEach((entry) => {
        // entry.interactionId 标识一次交互
        // entry.duration 包含输入延迟 + 处理时间 + 呈现时间
        // entry.processingStart - entry.startTime = 输入延迟
        // entry.processingEnd - entry.processingStart = 处理时间
        // entry.duration - (entry.processingEnd - entry.startTime) = 呈现时间

        const interactionDelay = entry.processingStart - entry.startTime;
        const processingTime = entry.processingEnd - entry.processingStart;
        const presentationTime = entry.duration - (entry.processingEnd - entry.startTime);

        console.log('交互事件:', entry.name);
        console.log('  输入延迟:', interactionDelay, 'ms');
        console.log('  处理时间:', processingTime, 'ms');
        console.log('  呈现时间:', presentationTime, 'ms');
        console.log('  总延迟:', entry.duration, 'ms');

        allInteractions.push({
          interactionId: entry.interactionId,
          eventType: entry.name,
          duration: entry.duration,
          inputDelay: interactionDelay,
          processingTime,
          presentationTime,
          target: entry.target?.tagName,
        });
      });

      // INP 取所有交互中的 P98 值(或最差值,取决于实现)
      // 简化实现:取最差值
      const maxDuration = Math.max(...allInteractions.map(i => i.duration));
      worstINP = maxDuration;
    });

    po.observe({ type: 'event', buffered: true, durationThreshold: 16 });

    // 页面隐藏时上报最终 INP
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        // 正式计算 INP:取 P98 分位值
        allInteractions.sort((a, b) => a.duration - b.duration);
        const p98Index = Math.floor(allInteractions.length * 0.98);
        const inp = allInteractions[p98Index]?.duration ?? worstINP;

        reportMetric({
          name: 'INP',
          value: inp,
          rating: inp <= 200 ? 'good' : inp <= 500 ? 'needs-improvement' : 'poor',
          interactionCount: allInteractions.length,
        });

        po.disconnect();
      }
    });
  } catch (e) {
    // 浏览器不支持
  }
}

INP 优化方向:

优化方向具体手段
减少主线程工作拆分长任务、scheduler.yield()requestIdleCallback
减少脚本执行时间Code Splitting、Tree Shaking、减少依赖体积
优化事件回调防抖节流、减少 DOM 操作、离屏计算
Web Worker将计算密集型任务移至 Worker 线程
请求动画帧将视觉更新与浏览器渲染对齐

CLS — Cumulative Layout Shift(累积布局偏移)

衡量页面视觉稳定性,统计页面生命周期内所有意外布局偏移的累计分数。用户正在阅读的内容突然跳动是非常差的体验。

评分阈值
≤ 0.1
需改进0.1 ~ 0.25
> 0.25

布局偏移分数计算:

CLS = Σ (影响分数 × 距离分数)

影响分数 = 不稳定元素在视口中占比
距离分数 = 不稳定元素移动的最大距离 / 视口尺寸
// 采集 CLS
function observeCLS() {
  if (!('PerformanceObserver' in window)) return;

  let clsValue = 0;
  let clsEntries = [];
  let sessionValue = 0;
  let sessionEntries = [];

  try {
    const po = new PerformanceObserver((entryList) => {
      entryList.getEntries().forEach((entry) => {
        // 仅统计非用户交互导致的布局偏移
        if (!entry.hadRecentInput) {
          const firstSessionEntry = sessionEntries[0];
          const lastSessionEntry = sessionEntries[sessionEntries.length - 1];

          // 会话窗口:如果距上一次偏移 < 1s 且会话总时长 < 5s,归为同一会话
          if (
            sessionValue &&
            entry.startTime - lastSessionEntry.startTime < 1000 &&
            entry.startTime - firstSessionEntry.startTime < 5000
          ) {
            sessionValue += entry.value;
            sessionEntries.push(entry);
          } else {
            sessionValue = entry.value;
            sessionEntries = [entry];
          }

          // 取所有会话中的最大值作为 CLS
          if (sessionValue > clsValue) {
            clsValue = sessionValue;
            clsEntries = [...sessionEntries];

            console.log('CLS 更新:', clsValue);
            console.log('偏移元素:', entry.sources?.map(s => s.node?.tagName));
          }
        }
      });
    });

    po.observe({ type: 'layout-shift', buffered: true });

    // 页面隐藏时上报
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        reportMetric({
          name: 'CLS',
          value: clsValue,
          rating: clsValue <= 0.1 ? 'good' : clsValue <= 0.25 ? 'needs-improvement' : 'poor',
          shiftCount: clsEntries.length,
          sources: clsEntries.flatMap(e => e.sources?.map(s => s.node?.tagName) ?? []),
        });
        po.disconnect();
      }
    });
  } catch (e) {
    // 浏览器不支持
  }
}

CLS 优化方向:

优化方向兺体手段
图片/视频尺寸始终设置 width/heightaspect-ratio
广告/嵌入预留占位空间、使用 min-height
动态内容在视口下方插入内容、避免现有内容上移
字体加载font-display: swapoptionalsize-adjust
SSR/SSG服务端渲染减少客户端动态注入

2. 其他 Web Vitals

指标全称含义好的阈值测量方式
TTFBTime to First Byte从请求发起到收到第一个字节的耗时≤ 800msNavigation Timing
FCPFirst Contentful Paint首次有内容绘制(文本、图片等)≤ 1.8sPaint Timing
TBTTotal Blocking TimeFCP 到 TTI 之间所有长任务阻塞时间之和≤ 200msLong Task API
SISpeed Index页面视觉内容展现速度≤ 3.4sLighthouse 合成
TTITime to Interactive页面完全可交互时间≤ 3.8sLighthouse 合成
// 采集 TTFB
function measureTTFB() {
  const [navEntry] = performance.getEntriesByType('navigation');
  if (navEntry) {
    const ttfb = navEntry.responseStart - navEntry.requestStart;
    console.log('TTFB:', ttfb, 'ms');
    return ttfb;
  }
  return -1;
}

// 采集 FCP
function observeFCP() {
  try {
    const po = new PerformanceObserver((entryList) => {
      const entries = entryList.getEntriesByName('first-contentful-paint');
      entries.forEach((entry) => {
        console.log('FCP:', entry.startTime, 'ms');
        reportMetric({
          name: 'FCP',
          value: entry.startTime,
          rating: entry.startTime <= 1800 ? 'good' :
                  entry.startTime <= 3000 ? 'needs-improvement' : 'poor',
        });
      });
    });
    po.observe({ type: 'paint', buffered: true });
  } catch (e) {}
}

// 计算 TBT(需结合 Long Task API)
function calculateTBT() {
  const [navEntry] = performance.getEntriesByType('navigation');
  if (!navEntry) return -1;

  const fcpTime = navEntry.loadEventEnd || navEntry.domContentLoadedEventEnd;
  const ttiEstimate = fcpTime + 5000; // TTI 估算简化

  let tbt = 0;
  performance.getEntriesByType('longtask').forEach((entry) => {
    if (entry.startTime >= fcpTime && entry.startTime < ttiEstimate) {
      const blockingTime = entry.duration - 50; // 超过 50ms 的部分为阻塞时间
      tbt += blockingTime;
    }
  });

  console.log('TBT:', tbt, 'ms');
  return tbt;
}

3. Performance API 采集

PerformanceObserver 详解

PerformanceObserver 是采集性能数据的核心 API,基于观察者模式,可在性能条目产生时异步回调,避免轮询。

/**
 * PerformanceObserver 完整使用示例
 * 支持的 entryType 列表:
 * - navigation:  页面导航性能
 * - resource:    资源加载性能
 * - paint:       绘制时间点(FCP、FP)
 * - largest-contentful-paint: LCP
 * - layout-shift:  布局偏移
 * - event:         交互事件(INP)
 * - longtask:      长任务
 * - element:       元素可见时间
 * - measure:       自定义度量
 * - mark:          自定义标记
 */

class PerformanceCollector {
  constructor() {
    this.observers = [];
    this.metrics = {};
  }

  // 注册 Observer
  observe(entryType, callback) {
    if (!('PerformanceObserver' in window)) return;

    try {
      const supportedTypes = PerformanceObserver.supportedEntryTypes;
      if (!supportedTypes.includes(entryType)) {
        console.warn(`Entry type "${entryType}" not supported`);
        return;
      }

      const po = new PerformanceObserver((list) => {
        // 使用 requestIdleCallback 避免阻塞主线程
        const processEntries = () => {
          const entries = list.getEntries();
          callback(entries);
        };

        if ('requestIdleCallback' in window) {
          requestIdleCallback(processEntries, { timeout: 2000 });
        } else {
          setTimeout(processEntries, 0);
        }
      });

      po.observe({ type: entryType, buffered: true });
      this.observers.push(po);
    } catch (e) {
      console.warn(`Failed to observe ${entryType}:`, e.message);
    }
  }

  // 断开所有 Observer
  disconnect() {
    this.observers.forEach(po => po.disconnect());
    this.observers = [];
  }

  // 采集所有核心指标
  collectAll() {
    // Navigation Timing
    this.observe('navigation', (entries) => {
      const nav = entries[entries.length - 1];
      this.metrics.navigation = {
        dns: nav.domainLookupEnd - nav.domainLookupStart,
        tcp: nav.connectEnd - nav.connectStart,
        ssl: nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0,
        ttfb: nav.responseStart - nav.requestStart,
        download: nav.responseEnd - nav.responseStart,
        domParsing: nav.domInteractive - nav.responseEnd,
        domComplete: nav.domComplete - nav.domInteractive,
        loadEvent: nav.loadEventEnd - nav.loadEventStart,
        total: nav.loadEventEnd - nav.startTime,
        transferSize: nav.transferSize,
        decodedBodySize: nav.decodedBodySize,
      };
    });

    // Paint Timing
    this.observe('paint', (entries) => {
      entries.forEach((entry) => {
        this.metrics[entry.name] = entry.startTime;
      });
    });

    // LCP
    this.observe('largest-contentful-paint', (entries) => {
      const lastEntry = entries[entries.length - 1];
      this.metrics.lcp = {
        startTime: lastEntry.startTime,
        size: lastEntry.size,
        element: lastEntry.element?.tagName,
        url: lastEntry.url,
      };
    });

    // CLS
    let clsValue = 0;
    this.observe('layout-shift', (entries) => {
      entries.forEach((entry) => {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
        }
      });
      this.metrics.cls = clsValue;
    });

    // Long Task
    this.observe('longtask', (entries) => {
      this.metrics.longTasks = entries.map(e => ({
        startTime: e.startTime,
        duration: e.duration,
        name: e.name,
        attribution: e.attribution?.map(a => ({
          name: a.name,
          containerType: a.containerType,
          containerName: a.containerName,
        })),
      }));
    });

    // Event Timing(INP)
    this.observe('event', (entries) => {
      entries.forEach((entry) => {
        if (!this.metrics.inp || entry.duration > this.metrics.inp) {
          this.metrics.inp = entry.duration;
        }
      });
    }, { durationThreshold: 16 }); // 降低阈值以捕获更多交互
  }
}

// 使用
const collector = new PerformanceCollector();
collector.collectAll();

// 页面卸载时获取所有数据
window.addEventListener('pagehide', () => {
  const data = collector.metrics;
  reportMetrics(data);
  collector.disconnect();
});

自定义 mark 和 measure

// 自定义标记(mark):记录时间点
performance.mark('data-fetch-start');
const response = await fetch('/api/data');
performance.mark('data-fetch-end');

// 自制度量(measure):计算区间耗时
performance.measure('data-fetch-duration', 'data-fetch-start', 'data-fetch-end');

// 读取自定义度量
const measures = performance.getEntriesByType('measure');
measures.forEach((measure) => {
  console.log(`${measure.name}: ${measure.duration}ms`);
});

// ---- 实际业务场景 ----

// 1. 首屏业务数据可用时间
performance.mark('page-start');
performance.mark('skeleton-rendered');       // 骨架屏渲染
performance.mark('data-request-sent');       // 数据请求发出
performance.mark('data-response-received');  // 数据响应接收
performance.mark('content-rendered');        // 内容渲染完成

performance.measure('skeleton-time', 'page-start', 'skeleton-rendered');
performance.measure('data-fetch-time', 'data-request-sent', 'data-response-received');
performance.measure('content-render-time', 'data-response-received', 'content-rendered');
performance.measure('first-meaningful-time', 'page-start', 'content-rendered');

// 2. 通过 PerformanceObserver 监听自定义度量
const measureObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    reportMetric({
      type: 'custom-measure',
      name: entry.name,
      duration: entry.duration,
      startTime: entry.startTime,
    });
  });
});
measureObserver.observe({ entryTypes: ['measure'] });

4. 资源加载监控

Resource Timing API

/**
 * Resource Timing API 提供每个资源的详细加载时序
 * 包含完整的网络链路时间点
 */
function observeResourceTiming() {
  const po = new PerformanceObserver((list) => {
    list.getEntries().forEach((entry) => {
      // 完整的资源加载时间线
      const resourceMetric = {
        name: entry.name,                         // 资源 URL
        initiatorType: entry.initiatorType,       // 发起类型(link/script/img等)
        transferSize: entry.transferSize,         // 传输大小
        encodedBodySize: entry.encodedBodySize,   // 编码后大小
        decodedBodySize: entry.decodedBodySize,   // 解码后大小

        // 网络时序(单位 ms)
        redirectTime: entry.redirectEnd - entry.redirectStart,
        dnsTime: entry.domainLookupEnd - entry.domainLookupStart,
        tcpTime: entry.connectEnd - entry.connectStart,
        sslTime: entry.secureConnectionStart > 0
          ? entry.connectEnd - entry.secureConnectionStart : 0,
        ttfb: entry.responseStart - entry.requestStart,
        downloadTime: entry.responseEnd - entry.responseStart,

        // 总耗时
        totalDuration: entry.duration,

        // 协议与连接信息
        protocol: entry.nextHopProtocol,          // h2、h3 等
        rendererBlocked: entry.renderBlockingStatus, // blocking / non-blocking
      };

      // 慢资源告警:超过 3s 的资源
      if (entry.duration > 3000) {
        console.warn('慢资源告警:', entry.name, entry.duration + 'ms');
        reportSlowResource(resourceMetric);
      }

      // 大资源告警:超过 500KB 的资源
      if (entry.transferSize > 500 * 1024) {
        console.warn('大资源告警:', entry.name, (entry.transferSize / 1024) + 'KB');
      }

      reportMetric({ type: 'resource', ...resourceMetric });
    });
  });

  po.observe({ type: 'resource', buffered: true });
}

关键资源瀑布图

/**
 * 构建资源加载瀑布图
 * 用于分析资源加载的并行度与瓶颈
 */
function buildResourceWaterfall() {
  const resources = performance.getEntriesByType('resource');

  // 按发起类型分组
  const grouped = resources.reduce((acc, entry) => {
    const type = entry.initiatorType;
    if (!acc[type]) acc[type] = [];
    acc[type].push({
      name: entry.name,
      startTime: Math.round(entry.startTime),
      duration: Math.round(entry.duration),
      transferSize: entry.transferSize,
    });
    return acc;
  }, {});

  // 按加载开始时间排序
  const timeline = resources
    .map((entry) => ({
      name: new URL(entry.name).pathname,
      type: entry.initiatorType,
      start: Math.round(entry.startTime),
      end: Math.round(entry.startTime + entry.duration),
      duration: Math.round(entry.duration),
      size: entry.transferSize,
    }))
    .sort((a, b) => a.start - b.start);

  // 识别关键路径
  const criticalPath = timeline
    .filter(r => r.type === 'script' || r.type === 'link')
    .filter(r => r.start < 2000); // 首屏关键资源

  console.table(criticalPath);
  return { grouped, timeline, criticalPath };
}

慢资源告警

/**
 * 慢资源告警系统
 * 按阈值分级告警
 */
class SlowResourceAlerter {
  constructor(config = {}) {
    this.thresholds = {
      duration: config.durationThreshold || 3000,     // 资源加载耗时
      transferSize: config.sizeThreshold || 500 * 1024, // 传输大小
      ttfb: config.ttfbThreshold || 1000,              // 资源 TTFB
    };
    this.alerts = [];
  }

  check(entry) {
    const alerts = [];

    if (entry.duration > this.thresholds.duration) {
      alerts.push({
        level: 'warning',
        type: 'slow-load',
        resource: entry.name,
        value: entry.duration,
        threshold: this.thresholds.duration,
        message: `资源加载耗时 ${Math.round(entry.duration)}ms 超过阈值 ${this.thresholds.duration}ms`,
      });
    }

    if (entry.transferSize > this.thresholds.transferSize) {
      alerts.push({
        level: 'info',
        type: 'large-size',
        resource: entry.name,
        value: entry.transferSize,
        threshold: this.thresholds.transferSize,
        message: `资源大小 ${(entry.transferSize / 1024).toFixed(1)}KB 超过阈值 ${(this.thresholds.transferSize / 1024).toFixed(1)}KB`,
      });
    }

    const resourceTTFB = entry.responseStart - entry.requestStart;
    if (resourceTTFB > this.thresholds.ttfb) {
      alerts.push({
        level: 'warning',
        type: 'slow-ttfb',
        resource: entry.name,
        value: resourceTTFB,
        threshold: this.thresholds.ttfb,
        message: `资源 TTFB ${Math.round(resourceTTFB)}ms 超过阈值 ${this.thresholds.ttfb}ms`,
      });
    }

    this.alerts.push(...alerts);
    return alerts;
  }
}

5. 长任务监控

Long Task API 监控执行时间超过 50ms 的任务,这些任务会阻塞主线程,导致交互响应延迟。

/**
 * 长任务监控完整实现
 */
function observeLongTasks() {
  if (!('PerformanceObserver' in window)) return;

  const longTasks = [];
  const LONG_TASK_THRESHOLD = 50; // 50ms 以上为长任务

  try {
    const po = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        const task = {
          startTime: entry.startTime,
          duration: entry.duration,
          name: entry.name,          // "self" / "same-origin-ancestor" / "cross-origin-ancestor"
          blockingDuration: entry.duration - LONG_TASK_THRESHOLD,
          attribution: entry.attribution?.map((a) => ({
            name: a.name,
            containerType: a.containerType,   // "window" / "iframe" / "sharedworker" 等
            containerName: a.containerName,
            containerSrc: a.containerSrc,
          })),
        };

        longTasks.push(task);

        // 实时告警:超长任务(> 500ms)
        if (entry.duration > 500) {
          console.error('严重长任务:', entry.duration + 'ms', entry.attribution);
        }
      });
    });

    po.observe({ type: 'longtask', buffered: true });

    // 页面隐藏时汇总上报
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden' && longTasks.length > 0) {
        // 计算阻塞时间总和(TBT 的一部分)
        const totalBlockingTime = longTasks.reduce(
          (sum, t) => sum + t.blockingDuration, 0
        );

        // 按来源归因
        const byAttribution = longTasks.reduce((acc, t) => {
          const source = t.attribution?.[0]?.containerType || 'unknown';
          if (!acc[source]) acc[source] = { count: 0, totalDuration: 0 };
          acc[source].count += 1;
          acc[source].totalDuration += t.duration;
          return acc;
        }, {});

        reportMetric({
          type: 'longtask-summary',
          count: longTasks.length,
          totalBlockingTime,
          maxDuration: Math.max(...longTasks.map(t => t.duration)),
          byAttribution,
        });
      }
    });
  } catch (e) {
    // Long Task API 不支持
  }
}

主线程阻塞分析

/**
 * 主线程阻塞可视化
 * 将长任务映射到时间轴上,分析阻塞模式
 */
function analyzeMainThreadBlocking() {
  const longTasks = performance.getEntriesByType('longtask');

  if (longTasks.length === 0) {
    return { hasBlocking: false };
  }

  // 按时间段分组(0-1s, 1-2s, 2-5s, 5-10s, 10s+)
  const timeSlots = [
    { label: '0-1s', start: 0, end: 1000, blocking: 0, count: 0 },
    { label: '1-2s', start: 1000, end: 2000, blocking: 0, count: 0 },
    { label: '2-5s', start: 2000, end: 5000, blocking: 0, count: 0 },
    { label: '5-10s', start: 5000, end: 10000, blocking: 0, count: 0 },
    { label: '10s+', start: 10000, end: Infinity, blocking: 0, count: 0 },
  ];

  longTasks.forEach((task) => {
    const slot = timeSlots.find(
      s => task.startTime >= s.start && task.startTime < s.end
    );
    if (slot) {
      slot.blocking += task.duration - 50;
      slot.count += 1;
    }
  });

  // 识别连续阻塞(多个长任务密集出现)
  let continuousBlocking = [];
  let currentStreak = [];

  longTasks.sort((a, b) => a.startTime - b.startTime);
  longTasks.forEach((task, i) => {
    if (i === 0 || task.startTime - longTasks[i - 1].startTime < 500) {
      currentStreak.push(task);
    } else {
      if (currentStreak.length >= 2) {
        continuousBlocking.push([...currentStreak]);
      }
      currentStreak = [task];
    }
  });
  if (currentStreak.length >= 2) {
    continuousBlocking.push(currentStreak);
  }

  return {
    hasBlocking: true,
    totalTasks: longTasks.length,
    totalBlockingTime: longTasks.reduce((s, t) => s + (t.duration - 50), 0),
    maxTaskDuration: Math.max(...longTasks.map(t => t.duration)),
    timeSlots,
    continuousBlocking: continuousBlocking.map(streak => ({
      start: streak[0].startTime,
      end: streak[streak.length - 1].startTime + streak[streak.length - 1].duration,
      taskCount: streak.length,
      totalBlocking: streak.reduce((s, t) => s + t.duration, 0),
    })),
  };
}

6. 内存与 FPS 监控

内存监控

/**
 * 内存使用监控
 * performance.memory 是非标准 API,Chrome 系浏览器支持
 */
function monitorMemory() {
  if (!performance.memory) {
    console.warn('performance.memory 不可用');
    return null;
  }

  const memory = performance.memory;
  const result = {
    usedJSHeapSize: memory.usedJSHeapSize,       // 已使用的 JS 堆大小
    totalJSHeapSize: memory.totalJSHeapSize,     // 当前 JS 堆总大小
    jsHeapSizeLimit: memory.jsHeapSizeLimit,     // JS 堆大小上限
    usedRatio: (memory.usedJSHeapSize / memory.jsHeapSizeLimit * 100).toFixed(2) + '%',
  };

  // 内存泄漏预警:使用量超过 80%
  if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > 0.8) {
    console.warn('内存使用率过高:', result.usedRatio);
    reportMetric({
      type: 'memory-warning',
      ...result,
    });
  }

  return result;
}

/**
 * 内存泄漏检测:周期性采样,检测持续增长趋势
 */
class MemoryLeakDetector {
  constructor(options = {}) {
    this.sampleInterval = options.sampleInterval || 5000; // 5s 采样一次
    this.maxSamples = options.maxSamples || 60;            // 最多保存 60 个样本
    this.samples = [];
    this.timer = null;
    this.growthThreshold = options.growthThreshold || 0.1; // 10% 增长告警
  }

  start() {
    this.timer = setInterval(() => {
      const data = monitorMemory();
      if (data) {
        this.samples.push({
          timestamp: Date.now(),
          ...data,
        });

        // 超过最大样本数,移除最旧的
        if (this.samples.length > this.maxSamples) {
          this.samples.shift();
        }

        // 检测增长趋势
        this.detectLeak();
      }
    }, this.sampleInterval);
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }

  detectLeak() {
    if (this.samples.length < 10) return; // 至少 10 个样本

    const first = this.samples[0];
    const last = this.samples[this.samples.length - 1];
    const growth = (last.usedJSHeapSize - first.usedJSHeapSize) / first.usedJSHeapSize;

    if (growth > this.growthThreshold) {
      console.warn(
        `疑似内存泄漏:${this.samples.length} 个样本内增长 ${(growth * 100).toFixed(1)}%`
      );
      console.warn(`  起始: ${(first.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB`);
      console.warn(`  当前: ${(last.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB`);

      reportMetric({
        type: 'memory-leak-suspected',
        growth: growth,
        startSize: first.usedJSHeapSize,
        endSize: last.usedJSHeapSize,
        sampleCount: this.samples.length,
      });
    }
  }
}

// 使用
const leakDetector = new MemoryLeakDetector();
leakDetector.start();

FPS 帧率监控

/**
 * FPS 帧率采集
 * 使用 requestAnimationFrame 计算实际帧率
 */
class FPSMonitor {
  constructor(options = {}) {
    this.targetFPS = 60;
    this.frameTime = 1000 / this.targetFPS; // ~16.67ms
    this.slowThreshold = options.slowThreshold || 50; // 低于 50fps 告警
    this.frames = [];
    this.lastTime = 0;
    this.running = false;
    this.rafId = null;
  }

  start() {
    this.running = true;
    this.lastTime = performance.now();
    this.tick();
  }

  stop() {
    this.running = false;
    if (this.rafId) {
      cancelAnimationFrame(this.rafId);
    }
  }

  tick() {
    if (!this.running) return;

    const now = performance.now();
    const delta = now - this.lastTime;
    this.lastTime = now;

    const fps = 1000 / delta;

    this.frames.push({
      timestamp: now,
      fps: Math.round(fps),
      frameTime: delta,
    });

    // 仅保留最近 120 帧
    if (this.frames.length > 120) {
      this.frames.shift();
    }

    // 检测卡顿帧(帧时间 > 50ms,即 FPS < 20)
    if (delta > 50) {
      console.warn('卡顿帧:', delta.toFixed(1) + 'ms', 'FPS:', Math.round(fps));
    }

    this.rafId = requestAnimationFrame(() => this.tick());
  }

  getStats() {
    if (this.frames.length === 0) return null;

    const fpsValues = this.frames.map(f => f.fps);
    const avgFPS = fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length;
    const minFPS = Math.min(...fpsValues);
    const maxFPS = Math.max(...fpsValues);

    // 计算卡顿率(FPS < 30 的帧占比)
    const jankFrames = fpsValues.filter(fps => fps < 30).length;
    const jankRate = jankFrames / fpsValues.length;

    return {
      avgFPS: Math.round(avgFPS),
      minFPS,
      maxFPS,
      jankRate: (jankRate * 100).toFixed(2) + '%',
      frameCount: this.frames.length,
    };
  }
}

// 使用
const fpsMonitor = new FPSMonitor();
fpsMonitor.start();

// 定期上报 FPS 统计
setInterval(() => {
  const stats = fpsMonitor.getStats();
  if (stats) {
    reportMetric({ type: 'fps', ...stats });
  }
}, 10000); // 每 10s 上报一次

7. 自定义指标采集

Element Timing API

/**
 * 元素可见时间监控
 * 监听特定元素的渲染时间
 */
function observeElementTiming() {
  try {
    const po = new PerformanceObserver((entryList) => {
      entryList.getEntries().forEach((entry) => {
        console.log('元素可见:', entry.identifier, entry.startTime + 'ms');
        console.log('  元素:', entry.element?.tagName, entry.element?.id);
        console.log('  渲染类型:', entry.renderTime ? 'renderTime' : 'loadTime');
        console.log('  时间:', entry.renderTime || entry.loadTime);
        console.log('  大小:', entry.size);

        reportMetric({
          type: 'element-timing',
          identifier: entry.identifier,
          startTime: entry.startTime,
          renderTime: entry.renderTime,
          loadTime: entry.loadTime,
          size: entry.size,
          element: entry.element?.tagName + '#' + entry.element?.id,
        });
      });
    });

    po.observe({ type: 'element', buffered: true });
  } catch (e) {
    // Element Timing API 不支持
  }
}

// HTML 中标记需要监控的元素
// <img elementtiming="hero-image" src="...">
// <h1 elementtiming="main-title">标题</h1>
// <div elementtiming="product-card">商品卡片</div>

自定义业务指标

/**
 * 自定义业务性能指标
 * 采集业务关键路径的耗时
 */
class BusinessMetricsCollector {
  constructor() {
    this.marks = {};
    this.measures = {};
  }

  // 标记时间点
  mark(name) {
    const time = performance.now();
    this.marks[name] = time;
    performance.mark(name);
    return time;
  }

  // 计算区间耗时
  measure(name, startMark, endMark) {
    try {
      performance.measure(name, startMark, endMark);
      const [measure] = performance.getEntriesByName(name, 'measure');
      if (measure) {
        this.measures[name] = measure.duration;
        return measure.duration;
      }
    } catch (e) {
      // 使用内部记录的 mark 回退
      if (this.marks[startMark] && this.marks[endMark]) {
        const duration = this.marks[endMark] - this.marks[startMark];
        this.measures[name] = duration;
        return duration;
      }
    }
    return -1;
  }

  // 上报所有业务指标
  report() {
    return Object.entries(this.measures).map(([name, duration]) => ({
      type: 'business-metric',
      name,
      duration,
    }));
  }
}

// ---- 实际业务场景 ----

const bizMetrics = new BusinessMetricsCollector();

// 电商场景:商品详情页
bizMetrics.mark('page-start');
// ... 骨架屏渲染
bizMetrics.mark('skeleton-ready');
// ... 商品数据返回
bizMetrics.mark('product-data-ready');
// ... SKU 选择器渲染
bizMetrics.mark('sku-selector-ready');
// ... 加购按钮可点击
bizMetrics.mark('add-cart-interactive');

bizMetrics.measure('skeleton-render', 'page-start', 'skeleton-ready');
bizMetrics.measure('product-data-fetch', 'skeleton-ready', 'product-data-ready');
bizMetrics.measure('sku-selector-render', 'product-data-ready', 'sku-selector-ready');
bizMetrics.measure('first-interactive-time', 'page-start', 'add-cart-interactive');

// 上报
window.addEventListener('pagehide', () => {
  const metrics = bizMetrics.report();
  reportMetrics(metrics);
});

8. 数据上报与聚合

采样策略

/**
 * 数据上报采样策略
 * 高流量场景下控制数据量
 */
class SamplingStrategy {
  constructor(config = {}) {
    this.globalSampleRate = config.globalSampleRate ?? 1.0;   // 全局采样率
    this.metricSampleRates = config.metricSampleRates ?? {};  // 按指标采样率
    this.userSampleRate = config.userSampleRate ?? 1.0;       // 用户采样率
  }

  // 基于用户 ID 的稳定采样(同一用户采样结果一致)
  shouldSampleUser(userId) {
    if (this.userSampleRate >= 1.0) return true;
    // 哈希后取模,保证同一用户结果稳定
    const hash = this.simpleHash(userId);
    return (hash % 10000) / 10000 < this.userSampleRate;
  }

  // 基于指标类型的采样
  shouldSampleMetric(metricName) {
    const rate = this.metricSampleRates[metricName] ?? this.globalSampleRate;
    return Math.random() < rate;
  }

  // 动态采样:根据指标评级调整采样率(差体验全量采样,好体验低采样)
  shouldSampleByRating(rating) {
    const rates = {
      'poor': 1.0,              // 差体验:全量上报
      'needs-improvement': 0.5, // 需改进:50% 上报
      'good': 0.1,              // 好体验:10% 上报
    };
    return Math.random() < (rates[rating] ?? this.globalSampleRate);
  }

  // 简单哈希函数
  simpleHash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 转为 32 位整数
    }
    return Math.abs(hash);
  }
}

// 配置示例
const sampler = new SamplingStrategy({
  globalSampleRate: 0.2,     // 全局 20%
  metricSampleRates: {
    'LCP': 1.0,              // LCP 全量
    'INP': 1.0,              // INP 全量
    'CLS': 1.0,              // CLS 全量
    'FCP': 0.5,              // FCP 50%
    'resource': 0.05,        // 资源 5%(量大)
    'longtask': 0.1,         // 长任务 10%
  },
  userSampleRate: 0.3,      // 30% 用户
});

数据上报机制

/**
 * 数据上报管理器
 * 支持批量上报、离线缓存、sendBeacon
 */
class ReportManager {
  constructor(config = {}) {
    this.endpoint = config.endpoint || '/api/perf';
    this.batchSize = config.batchSize || 10;
    this.maxQueueTime = config.maxQueueTime || 5000;  // 最长队列等待时间
    this.queue = [];
    this.timer = null;
  }

  // 添加数据到队列
  add(data) {
    this.queue.push({
      ...data,
      timestamp: Date.now(),
      url: location.href,
      userAgent: navigator.userAgent,
      // 附加环境信息
      connection: navigator.connection ? {
        effectiveType: navigator.connection.effectiveType, // 4g/3g/2g
        downlink: navigator.connection.downlink,
        rtt: navigator.connection.rtt,
      } : null,
    });

    if (this.queue.length >= this.batchSize) {
      this.flush();
    } else if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), this.maxQueueTime);
    }
  }

  // 批量上报
  flush() {
    if (this.queue.length === 0) return;

    const data = [...this.queue];
    this.queue = [];
    clearTimeout(this.timer);
    this.timer = null;

    this.send(data);
  }

  // 发送数据(优先 sendBeacon)
  send(data) {
    const payload = JSON.stringify(data);

    // 优先使用 sendBeacon(页面卸载时仍可发送)
    if (navigator.sendBeacon) {
      const blob = new Blob([payload], { type: 'application/json' });
      const success = navigator.sendBeacon(this.endpoint, blob);
      if (success) return;
    }

    // 回退到 fetch(keepalive 确保页面卸载时发送)
    fetch(this.endpoint, {
      method: 'POST',
      body: payload,
      headers: { 'Content-Type': 'application/json' },
      keepalive: true,
    }).catch(() => {
      // 发送失败,存入 localStorage 等下次发送
      this.storeOffline(data);
    });
  }

  // 离线存储
  storeOffline(data) {
    try {
      const key = 'perf_offline_queue';
      const existing = JSON.parse(localStorage.getItem(key) || '[]');
      existing.push(...data);
      // 最多存储 100 条
      if (existing.length > 100) existing.splice(0, existing.length - 100);
      localStorage.setItem(key, JSON.stringify(existing));
    } catch (e) {
      // localStorage 可能不可用
    }
  }

  // 重发离线数据
  resendOffline() {
    try {
      const key = 'perf_offline_queue';
      const data = JSON.parse(localStorage.getItem(key) || '[]');
      if (data.length > 0) {
        this.send(data);
        localStorage.removeItem(key);
      }
    } catch (e) {}
  }
}

// 页面卸载时确保上报
window.addEventListener('pagehide', () => {
  reportManager.flush();
});

// 页面恢复时重发离线数据
window.addEventListener('pageshow', (e) => {
  if (e.persisted) {
    reportManager.resendOffline();
  }
});

聚合分析:分位值与维度分布

/**
 * 性能数据聚合分析(服务端逻辑示例)
 * 计算 P50/P75/P95/P99 分位值和多维度分布
 */
class MetricsAggregator {
  constructor() {
    this.records = [];
  }

  add(record) {
    this.records.push(record);
  }

  // 计算分位值
  static percentile(sortedValues, p) {
    const idx = Math.ceil(sortedValues.length * p / 100) - 1;
    return sortedValues[Math.max(0, idx)];
  }

  // 按指标名聚合
  aggregateByMetric(metricName) {
    const values = this.records
      .filter(r => r.name === metricName)
      .map(r => r.value)
      .sort((a, b) => a - b);

    if (values.length === 0) return null;

    return {
      metric: metricName,
      count: values.length,
      min: values[0],
      max: values[values.length - 1],
      mean: values.reduce((a, b) => a + b, 0) / values.length,
      p50: MetricsAggregator.percentile(values, 50),
      p75: MetricsAggregator.percentile(values, 75),
      p90: MetricsAggregator.percentile(values, 90),
      p95: MetricsAggregator.percentile(values, 95),
      p99: MetricsAggregator.percentile(values, 99),
    };
  }

  // 多维度拆分
  aggregateByDimension(metricName, dimension) {
    // dimension: 'browser' | 'region' | 'device' | 'connection' | 'url'
    const groups = {};

    this.records
      .filter(r => r.name === metricName)
      .forEach(r => {
        const key = r[dimension] || 'unknown';
        if (!groups[key]) groups[key] = [];
        groups[key].push(r.value);
      });

    const result = {};
    Object.entries(groups).forEach(([key, values]) => {
      values.sort((a, b) => a - b);
      result[key] = {
        count: values.length,
        p50: MetricsAggregator.percentile(values, 50),
        p75: MetricsAggregator.percentile(values, 75),
        p95: MetricsAggregator.percentile(values, 95),
        p99: MetricsAggregator.percentile(values, 99),
      };
    });

    return result;
  }

  // Core Web Vitals 达标率
  computeCwvPassRate() {
    const lcpValues = this.records.filter(r => r.name === 'LCP').map(r => r.value);
    const inpValues = this.records.filter(r => r.name === 'INP').map(r => r.value);
    const clsValues = this.records.filter(r => r.name === 'CLS').map(r => r.value);

    return {
      LCP: {
        good: lcpValues.filter(v => v <= 2500).length / lcpValues.length,
        poor: lcpValues.filter(v => v > 4000).length / lcpValues.length,
      },
      INP: {
        good: inpValues.filter(v => v <= 200).length / inpValues.length,
        poor: inpValues.filter(v => v > 500).length / inpValues.length,
      },
      CLS: {
        good: clsValues.filter(v => v <= 0.1).length / clsValues.length,
        poor: clsValues.filter(v => v > 0.25).length / clsValues.length,
      },
    };
  }
}

分位值含义说明:

分位值含义典型用途
P50(中位数)50% 的用户体验在此值以下了解”大多数用户”的体验
P7575% 的用户体验在此值以下Google Search Console 使用 P75
P9090% 的用户体验在此值以下识别较差体验的边界
P9595% 的用户体验在此值以下性能告警常用阈值
P9999% 的用户体验在此值以下极端情况分析

维度分析示例:

维度分析价值典型发现
地域CDN 效果、边缘节点覆盖海外用户 TTFB 明显偏高
浏览器兼容性、API 支持差异Safari 某版本 CLS 异常
设备低端机性能瓶颈低端 Android INP 差
网络资源加载策略适配2G/3G 用户 FCP 极慢
页面 URL瓶颈页面定位某列表页 LCP 普遍超 4s

9. 性能告警与 SLO

告警规则

/**
 * 性能告警规则引擎
 */
class AlertRuleEngine {
  constructor() {
    this.rules = [];
  }

  // 添加告警规则
  addRule(rule) {
    this.rules.push({
      id: rule.id,
      name: rule.name,
      metric: rule.metric,           // 指标名
      condition: rule.condition,     // 'gt' | 'lt' | 'gte' | 'lte'
      threshold: rule.threshold,     // 阈值
      window: rule.window || '5m',   // 时间窗口
      minSamples: rule.minSamples || 10, // 最小样本数
      level: rule.level || 'warning',    // 'info' | 'warning' | 'critical'
      channels: rule.channels || ['email'], // 通知渠道
    });
  }

  // 评估规则
  evaluate(metricName, aggregatedData) {
    const triggered = [];

    this.rules
      .filter(r => r.metric === metricName)
      .forEach((rule) => {
        if (aggregatedData.count < rule.minSamples) return;

        const value = aggregatedData[rule.aggregation || 'p75'];
        let isTriggered = false;

        switch (rule.condition) {
          case 'gt':  isTriggered = value > rule.threshold; break;
          case 'gte': isTriggered = value >= rule.threshold; break;
          case 'lt':  isTriggered = value < rule.threshold; break;
          case 'lte': isTriggered = value <= rule.threshold; break;
        }

        if (isTriggered) {
          triggered.push({
            ruleId: rule.id,
            ruleName: rule.name,
            metric: rule.metric,
            currentValue: value,
            threshold: rule.threshold,
            level: rule.level,
            channels: rule.channels,
            message: `${rule.name}: ${metricName} ${rule.aggregation || 'p75'} = ${value}, 超过阈值 ${rule.threshold}`,
          });
        }
      });

    return triggered;
  }
}

// 配置典型告警规则
const engine = new AlertRuleEngine();

engine.addRule({
  id: 'lcp-p75-warning',
  name: 'LCP P75 告警',
  metric: 'LCP',
  condition: 'gt',
  threshold: 2500,
  aggregation: 'p75',
  level: 'warning',
  channels: ['slack', 'email'],
});

engine.addRule({
  id: 'lcp-p75-critical',
  name: 'LCP P75 严重告警',
  metric: 'LCP',
  condition: 'gt',
  threshold: 4000,
  aggregation: 'p75',
  level: 'critical',
  channels: ['slack', 'email', 'sms'],
});

engine.addRule({
  id: 'inp-p75-warning',
  name: 'INP P75 告警',
  metric: 'INP',
  condition: 'gt',
  threshold: 200,
  aggregation: 'p75',
  level: 'warning',
});

engine.addRule({
  id: 'cls-p75-warning',
  name: 'CLS P75 告警',
  metric: 'CLS',
  condition: 'gt',
  threshold: 0.1,
  aggregation: 'p75',
  level: 'warning',
});

engine.addRule({
  id: 'error-rate-spike',
  name: '错误率飙升',
  metric: 'jsErrorRate',
  condition: 'gt',
  threshold: 0.05, // 5%
  aggregation: 'mean',
  level: 'critical',
});

SLO 目标设定

# 性能 SLO 配置示例
slo:
  # Core Web Vitals SLO
  lcp:
    target: 0.75          # 75% 的页面访问 LCP < 2.5s
    threshold: 2500       # ms
    window: 28d           # 28 天滚动窗口

  inp:
    target: 0.75          # 75% 的页面访问 INP < 200ms
    threshold: 200
    window: 28d

  cls:
    target: 0.75          # 75% 的页面访问 CLS < 0.1
    threshold: 0.1
    window: 28d

  # 可用性 SLO
  availability:
    target: 0.999         # 99.9% 可用性
    window: 30d

  # 自定义业务 SLO
  search-result-render:
    target: 0.90          # 90% 的搜索结果渲染 < 1s
    threshold: 1000
    window: 7d

# 错误预算(Error Budget)
error_budget:
  lcp:
    monthly_budget: 25%   # 最多 25% 的访问可以不达标
    burn_rate_alert:
      fast: 14.4x         # 1 小时耗尽预算
      slow: 6x            # 6 小时耗尽预算

性能预算 Performance Budget

/**
 * 性能预算配置与检查
 * 用于 CI/CD 流水线中的性能门禁
 */

// performance-budget.js
const budget = {
  // 资源体积预算
  resourceSizes: [
    { resourceType: 'script', budget: 200 },     // JS 总体积 < 200KB
    { resourceType: 'stylesheet', budget: 50 },   // CSS 总体积 < 50KB
    { resourceType: 'image', budget: 300 },        // 图片总体积 < 300KB
    { resourceType: 'font', budget: 80 },          // 字体总体积 < 80KB
    { resourceType: 'total', budget: 800 },        // 页面总体积 < 800KB
  ],

  // 资源数量预算
  resourceCounts: [
    { resourceType: 'script', budget: 10 },       // JS 文件数 < 10
    { resourceType: 'stylesheet', budget: 3 },    // CSS 文件数 < 3
    { resourceType: 'image', budget: 15 },        // 图片数 < 15
    { resourceType: 'font', budget: 4 },          // 字体数 < 4
    { resourceType: 'third-party', budget: 5 },   // 第三方资源 < 5
  ],

  // 性能指标预算(毫秒)
  metrics: [
    { metric: 'LCP', budget: 2500 },
    { metric: 'INP', budget: 200 },
    { metric: 'CLS', budget: 0.1 },
    { metric: 'TTFB', budget: 800 },
    { metric: 'FCP', budget: 1800 },
    { metric: 'TBT', budget: 200 },
  ],

  // 自定义规则
  custom: [
    {
      name: 'no-render-blocking-resources',
      description: '不应有渲染阻塞资源',
      check: (har) => {
        return !har.entries.some(e =>
          e.initiatorType === 'link' && e.renderBlockingStatus === 'blocking'
        );
      },
    },
    {
      name: 'image-optimization',
      description: '图片应使用现代格式',
      check: (har) => {
        const images = har.entries.filter(e => e.initiatorType === 'img');
        return images.every(e =>
          e.name.includes('.webp') || e.name.includes('.avif')
        );
      },
    },
  ],
};

// ---- CI/CD 中的性能门禁 ----

// 使用 Lighthouse CI
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/'],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.85 }],
        'first-contentful-paint': ['error', { maxNumericValue: 1800 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'interactive': ['error', { maxNumericValue: 3800 }],
        'total-blocking-time': ['error', { maxNumericValue: 200 }],
        'resource-summary:script:size': ['error', { maxNumericValue: 200000 }],
        'resource-summary:stylesheet:size': ['error', { maxNumericValue: 50000 }],
        'resource-summary:image:size': ['error', { maxNumericValue: 300000 }],
      },
    },
    upload: {
      target: 'lhci',
      serverBaseUrl: 'http://localhost:9001',
    },
  },
};

10. 可视化看板

Grafana 看板配置

{
  "dashboard": {
    "title": "前端性能监控",
    "panels": [
      {
        "title": "Core Web Vitals 趋势",
        "type": "timeseries",
        "targets": [
          {
            "expr": "histogram_quantile(0.75, rate(perf_lcp_bucket[1h]))",
            "legendFormat": "LCP P75"
          },
          {
            "expr": "histogram_quantile(0.75, rate(perf_inp_bucket[1h]))",
            "legendFormat": "INP P75"
          }
        ],
        "fieldConfig": {
          "thresholds": {
            "steps": [
              { "value": null, "color": "green" },
              { "value": 2500, "color": "yellow" },
              { "value": 4000, "color": "red" }
            ]
          }
        }
      },
      {
        "title": "CWV 达标率",
        "type": "gauge",
        "targets": [
          {
            "expr": "sum(rate(perf_lcp_bucket{le=\"2500\"}[7d])) / sum(rate(perf_lcp_bucket[7d]))",
            "legendFormat": "LCP Good Rate"
          }
        ],
        "fieldConfig": {
          "thresholds": {
            "steps": [
              { "value": null, "color": "red" },
              { "value": 0.5, "color": "yellow" },
              { "value": 0.75, "color": "green" }
            ]
          }
        }
      },
      {
        "title": "页面 LCP 分布",
        "type": "barchart",
        "targets": [
          {
            "expr": "sum by (page) (rate(perf_lcp_bucket{le=\"2500\"}[1d])) / sum by (page) (rate(perf_lcp_bucket[1d]))",
            "legendFormat": "{{page}}"
          }
        ]
      },
      {
        "title": "地域性能分布",
        "type": "geomap",
        "targets": [
          {
            "expr": "histogram_quantile(0.75, sum by (le, region) (rate(perf_lcp_bucket[1d])))",
            "legendFormat": "{{region}}"
          }
        ]
      }
    ]
  }
}

自建 Dashboard 数据接口

/**
 * 性能看板数据 API 设计
 * 提供聚合查询能力
 */

// API 路由设计
const apiRoutes = {
  // 核心指标概览
  'GET /api/perf/overview': {
    params: { timeRange: '7d', granularity: '1h' },
    response: {
      lcp: { p50: 1200, p75: 2100, p95: 3800, p99: 6200, goodRate: 0.78 },
      inp: { p50: 80, p75: 150, p95: 320, p99: 580, goodRate: 0.82 },
      cls: { p50: 0.02, p75: 0.08, p95: 0.18, p99: 0.35, goodRate: 0.76 },
    },
  },

  // 指标趋势
  'GET /api/perf/trend': {
    params: { metric: 'LCP', percentile: 'p75', timeRange: '30d', granularity: '1d' },
    response: {
      data: [
        { time: '2026-04-12', value: 2300 },
        { time: '2026-04-13', value: 2250 },
        // ...
      ],
    },
  },

  // 维度分布
  'GET /api/perf/dimension': {
    params: { metric: 'LCP', dimension: 'browser', percentile: 'p75' },
    response: {
      Chrome: { p75: 1900, count: 45000 },
      Safari: { p75: 2400, count: 12000 },
      Firefox: { p75: 2100, count: 8000 },
    },
  },

  // 页面性能排名
  'GET /api/perf/pages': {
    params: { metric: 'LCP', sort: 'p75', order: 'desc', limit: 20 },
    response: [
      { url: '/products/list', p75: 4200, count: 5600 },
      { url: '/search', p75: 3800, count: 3200 },
      // ...
    ],
  },

  // 慢资源排行
  'GET /api/perf/slow-resources': {
    params: { threshold: 3000, limit: 20 },
    response: [
      { url: '/static/vendor.js', avgDuration: 2800, p95: 4500, count: 12000 },
      // ...
    ],
  },
};

11. 优化闭环

性能监控的终极目标不是”看到数据”,而是形成从监控到优化的闭环

  ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
  │  监控采集 │────▶│  分析定位 │────▶│  优化实施 │────▶│  验证回归 │
  │  指标数据 │     │  瓶颈归因 │     │  代码改动 │     │  效果确认 │
  └────▲─────┘     └──────────┘     └──────────┘     └────┬─────┘
       │                                                    │
       └────────────────────────────────────────────────────┘
                            持续循环

A/B 测试性能对比

/**
 * A/B 测试性能对比
 * 确保优化确实生效
 */
class ABPerfTest {
  constructor(config) {
    this.experimentId = config.experimentId;
    this.variants = config.variants; // { control: 0.5, treatment: 0.5 }
    this.metrics = config.metrics;   // ['LCP', 'INP', 'CLS']
  }

  // 分配实验组
  assignVariant(userId) {
    const hash = this.simpleHash(userId + this.experimentId);
    let cumulative = 0;
    for (const [variant, ratio] of Object.entries(this.variants)) {
      cumulative += ratio;
      if ((hash % 10000) / 10000 < cumulative) {
        return variant;
      }
    }
    return Object.keys(this.variants)[0];
  }

  // 记录实验数据
  record(userId, variant, metrics) {
    return {
      experimentId: this.experimentId,
      variant,
      userId,
      metrics,
      timestamp: Date.now(),
    };
  }

  // 统计显著性检验(简化版 Z-test)
  analyze(results) {
    const byVariant = {};

    results.forEach(r => {
      if (!byVariant[r.variant]) byVariant[r.variant] = [];
      byVariant[r.variant].push(r.metrics);
    });

    const analysis = {};
    this.metrics.forEach((metric) => {
      const controlValues = byVariant.control.map(m => m[metric]);
      const treatmentValues = byVariant.treatment.map(m => m[metric]);

      const controlMean = controlValues.reduce((a, b) => a + b, 0) / controlValues.length;
      const treatmentMean = treatmentValues.reduce((a, b) => a + b, 0) / treatmentValues.length;

      const controlStd = Math.sqrt(
        controlValues.reduce((s, v) => s + (v - controlMean) ** 2, 0) / controlValues.length
      );
      const treatmentStd = Math.sqrt(
        treatmentValues.reduce((s, v) => s + (v - treatmentMean) ** 2, 0) / treatmentValues.length
      );

      const se = Math.sqrt(
        controlStd ** 2 / controlValues.length + treatmentStd ** 2 / treatmentValues.length
      );
      const zScore = (treatmentMean - controlMean) / se;
      const pValue = 2 * (1 - this.normalCDF(Math.abs(zScore)));

      analysis[metric] = {
        controlMean: Math.round(controlMean),
        treatmentMean: Math.round(treatmentMean),
        improvement: ((treatmentMean - controlMean) / controlMean * 100).toFixed(2) + '%',
        zScore: zScore.toFixed(4),
        pValue: pValue.toFixed(6),
        significant: pValue < 0.05,
      };
    });

    return analysis;
  }

  // 标准正态 CDF 近似
  normalCDF(x) {
    const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
    const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
    const sign = x < 0 ? -1 : 1;
    x = Math.abs(x) / Math.sqrt(2);
    const t = 1.0 / (1.0 + p * x);
    const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
    return 0.5 * (1.0 + sign * y);
  }

  simpleHash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = ((hash << 5) - hash) + str.charCodeAt(i);
      hash = hash & hash;
    }
    return Math.abs(hash);
  }
}

上线后回归检测

/**
 * 发布后性能回归检测
 * 对比发布前后指标变化
 */
class RegressionDetector {
  constructor(config) {
    this.beforeWindow = config.beforeWindow || 24 * 60 * 60 * 1000; // 发布前 24h
    this.afterWindow = config.afterWindow || 2 * 60 * 60 * 1000;   // 发布后 2h
    this.regressionThreshold = config.regressionThreshold || 0.15;  // 恶化 15% 告警
  }

  async detect(deployTime, metrics) {
    const beforeStart = deployTime - this.beforeWindow;
    const afterEnd = deployTime + this.afterWindow;

    const results = {};

    for (const metric of metrics) {
      const beforeData = await this.fetchMetrics(metric, beforeStart, deployTime);
      const afterData = await this.fetchMetrics(metric, deployTime, afterEnd);

      const beforeP75 = MetricsAggregator.percentile(beforeData.sort((a, b) => a - b), 75);
      const afterP75 = MetricsAggregator.percentile(afterData.sort((a, b) => a - b), 75);

      const change = (afterP75 - beforeP75) / beforeP75;
      const isRegression = change > this.regressionThreshold;

      results[metric] = {
        beforeP75: Math.round(beforeP75),
        afterP75: Math.round(afterP75),
        changePercent: (change * 100).toFixed(2) + '%',
        isRegression,
        severity: isRegression
          ? (change > 0.3 ? 'critical' : change > 0.2 ? 'warning' : 'info')
          : 'none',
      };

      if (isRegression) {
        console.error(
          `性能回归: ${metric} P75 从 ${Math.round(beforeP75)} 恶化至 ${Math.round(afterP75)}` +
          ` (${(change * 100).toFixed(2)}%)`
        );
      }
    }

    return results;
  }

  async fetchMetrics(metric, start, end) {
    // 从数据源查询指标数据
    // 实际实现对接 ClickHouse / Prometheus 等
    return [];
  }
}

常见问题

1. 数据噪声

问题:性能数据天然存在大量噪声——不同设备、网络、用户行为差异巨大,同一页面在不同条件下耗时可能差 10 倍以上。

解决方案

方法说明
分位值代替均值P75/P95 比 mean 更稳定,不受极端值影响
维度拆分按设备/网络/地域拆分后分别分析
异常值过滤剔除 robot 流量、预渲染、浏览器扩展干扰
足够样本量每个维度至少 100+ 样本再做判断
时间窗口取 7~28 天滚动窗口而非单日数据

2. 采样率选择

问题:全量采集数据量太大、成本高;采样率太低又可能漏掉关键信息。

策略

流量级别建议采样率说明
日 PV < 10 万100%数据量可控,全量采集
日 PV 10~100 万30%~50%Core Web Vitals 全量,资源/长任务采样
日 PV 100~1000 万10%~30%差体验全量,好体验低采样
日 PV > 1000 万1%~10%分层采样 + 差体验全量

关键原则:Core Web Vitals 采样率应保证 Google Search Console 所需的 P75 数据准确度——至少需要每月 1000+ 次有效采集。

3. INP 与 FID 差异

维度FID(旧)INP(新)
测量范围仅首次交互全页面生命周期所有交互
测量内容仅输入延迟输入延迟 + 处理时间 + 呈现时间
评分标准好 ≤ 100ms好 ≤ 200ms
代表性低(首次交互通常很快)高(反映最差交互体验)
典型问题首次点击不卡但后续卡持续交互卡顿也能捕捉
替代原因不反映整体交互体验更全面、更贴近真实感受

迁移注意:FID 好不代表 INP 好。许多页面首次交互(FID)很快,但滚动中的交互延迟(INP)很差。

4. 指标与业务不对应

问题:技术指标达标了,但业务指标(转化率、跳出率)没有改善。

原因

  • LCP 达标但首屏业务数据不可用(骨架屏 + 慢接口)
  • FCP 达标但内容不可交互(水合慢)
  • CLS 达标但关键 CTA 按钮位置不稳定

解决方案

  1. 自定义业务指标:采集”首屏业务数据可用时间”等真正影响业务的指标
  2. 关联分析:将性能指标与业务指标(转化率、停留时长)做关联分析
  3. 分层优化:不追求所有指标达标,而是聚焦影响业务的瓶颈指标
  4. 用户分群:对不同价值用户分别优化体验

面试题

1. Core Web Vitals 包含哪三大指标?各自的评分标准是什么?

Core Web Vitals 包含三大指标:

指标含义好的阈值需改进
LCP最大内容绘制,衡量加载性能≤ 2.5s2.5~4.0s> 4.0s
INP交互到下次绘制,衡量交互响应性≤ 200ms200~500ms> 500ms
CLS累积布局偏移,衡量视觉稳定性≤ 0.10.1~0.25> 0.25

2024 年 3 月,INP 正式替代 FID 成为交互指标。三者分别覆盖”快不快”(加载)、“灵不灵”(交互)、“稳不稳”(视觉)三个维度。


2. LCP 优化可以从哪些方向入手?

LCP 优化方向按影响链路从前往后排列:

  1. 服务端响应优化:CDN 缓存、Edge SSR/SSG、减少 TTFB
  2. 关键资源预加载<link rel="preload"> 加载 LCP 图片/字体,fetchpriority="high" 提升优先级
  3. 渲染阻塞消除:关键 CSS 内联、非关键 CSS 延迟加载、JS 使用 async/defer
  4. 图片优化:WebP/AVIF 格式、srcset 响应式尺寸、非首屏图片懒加载、CDN 图片处理
  5. 字体优化font-display: swap<link rel="preload"> 字体、子集化
  6. 代码优化:Code Splitting 减少首屏 JS 体积、Tree Shaking 移除无用代码

核心思路:缩短 LCP 元素从请求到渲染的完整链路。


3. INP 和 FID 的区别是什么?为什么 INP 替代了 FID?

维度FIDINP
测量范围仅首次交互全页面生命周期所有交互
测量内容仅输入延迟(从输入到事件回调开始)输入延迟 + 事件处理 + 呈现时间(完整延迟)
评分阈值好 ≤ 100ms好 ≤ 200ms
典型盲区首次交互后卡顿无法发现持续交互卡顿可捕捉

FID 被替代的根本原因:FID 只度量”第一次”交互的输入延迟,不反映后续交互体验。很多 SPA 首次交互很快(FID 好),但滚动中的事件处理、列表渲染导致严重延迟,FID 完全无法捕捉。INP 观察所有交互取 P98 值,更全面地反映用户真实体验。


4. PerformanceObserver 的用法是什么?与 performance.getEntries 有何区别?

PerformanceObserver 是基于观察者模式的异步性能数据监听 API:

const po = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => console.log(entry));
});
po.observe({ type: 'largest-contentful-paint', buffered: true });

performance.getEntries() 的区别

维度PerformanceObserverperformance.getEntries
获取方式异步回调,新条目产生时触发同步查询,获取已存在的条目
性能影响低,仅处理关注的类型高,遍历所有缓冲区条目
缓冲区压力不受缓冲区大小限制performance.bufferSize 限制
实时性高,条目产生即回调需要轮询
适用场景持续监听(LCP/CLS/Long Task)一次性快照查询

推荐使用 PerformanceObserver,仅在需要一次性查询时用 performance.getEntries()buffered: true 参数可以获取 observer 创建前的历史条目。


5. 如何实现性能预算(Performance Budget)?在 CI/CD 中如何落地?

性能预算为各项性能指标设定上限,防止性能劣化:

1. 定义预算

const budget = {
  metrics: { LCP: 2500, FCP: 1800, CLS: 0.1, TBT: 200 },
  resources: { script: 200, stylesheet: 50, image: 300 }, // KB
  counts: { script: 10, thirdParty: 5 },
};

2. CI/CD 落地方案

  • Lighthouse CI:在 PR 流水线中运行 Lighthouse,对比基准分数

    # GitHub Actions
    - name: Lighthouse CI
      uses: treosh/lighthouse-ci-action@v9
      with:
        configPath: .lighthouserc.js
        budgetPath: budget.json
  • Bundlewatch:监控打包体积变化

    { "files": [{ "path": "dist/*.js", "maxSize": "200KB" }] }
  • 自定义门禁脚本:对比 PR 分支与主分支的 Lighthouse 分数,恶化超过阈值则阻断合并

3. 预算策略

  • 渐进式收紧:初始设宽松阈值,逐步收紧
  • 分级告警:warning 不阻断但通知,error 阻断合并
  • 排除波动:多次运行取中位数,排除 CI 环境噪声

6. 长任务如何检测?如何优化长任务对主线程的阻塞?

检测方式

// 1. Long Task API
const po = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    console.log('长任务:', entry.duration + 'ms', entry.attribution);
  });
});
po.observe({ type: 'longtask', buffered: true });

// 2. 自定义检测(兼容性回退)
let lastTime = performance.now();
function detectLongTask() {
  const now = performance.now();
  const delta = now - lastTime;
  if (delta > 50) {
    console.warn('疑似长任务:', delta + 'ms');
  }
  lastTime = now;
  requestAnimationFrame(detectLongTask);
}

优化策略

  1. 任务拆分:将长任务拆为多个 < 50ms 的微任务

    // 使用 scheduler.yield() 让出主线程
    async function processChunked(items, chunkSize = 50) {
      for (let i = 0; i < items.length; i += chunkSize) {
        const chunk = items.slice(i, i + chunkSize);
        processChunk(chunk);
        await scheduler.yield(); // 让出主线程,允许交互响应
      }
    }
  2. Web Worker:计算密集型任务移至 Worker 线程

  3. requestIdleCallback:非紧急任务在浏览器空闲时执行

  4. 虚拟列表:大数据列表分批渲染,避免一次性 DOM 操作

  5. 防抖节流:高频事件(scroll/resize)控制回调频率


7. P75 和 P95 分别是什么含义?为什么性能监控常用 P75 而非均值?

  • P75:75% 的数据小于此值。即 75% 的用户体验在此值以下,25% 的用户体验更差
  • P95:95% 的数据小于此值。即 95% 的用户体验在此值以下,5% 的体验极差

为什么用 P75 而非均值

  1. 不受极端值影响:性能数据常有极端值(后台页面、插件干扰等),均值会被拉偏,P75 更稳健
  2. 代表”大多数用户”体验:P75 表示 3/4 用户的体验水平,比均值更直觉
  3. Google 标准对齐:Google Search Console 使用 P75 评估 Core Web Vitals 达标情况
  4. SLO 制定依据:性能 SLO 通常设为”P75 LCP < 2.5s”而非”mean LCP < 2.5s”

分位值选择建议

分位值适用场景
P50了解典型体验
P75SLO 目标、Google 评估
P95告警阈值、容量规划
P99极端情况排查、长尾优化

8. 如何设计性能监控的 SLO?包括目标设定、错误预算和告警策略。

SLO 设计三步法

第一步:设定目标

slo:
  lcp:
    target: 0.75          # 75% 请求 LCP < 2.5s
    threshold: 2500
    window: 28d
  inp:
    target: 0.75
    threshold: 200
    window: 28d
  cls:
    target: 0.75
    threshold: 0.1
    window: 28d

SLO 目标设定原则:

  • 初始基于现状数据(当前 P75 值)设定,再逐步收紧
  • 区分 SLI(指标,如 LCP P75)和 SLO(目标,如 75% < 2.5s)
  • 不同页面可设不同 SLO(首页更严格、二级页可适当放宽)

第二步:计算错误预算

错误预算 = 1 - SLO 目标率
例如:SLO = 75% 达标 → 错误预算 = 25%
28 天内 100 万次 PV → 最多 25 万次可以不达标

错误预算消耗速度(Burn Rate)决定告警级别:

Burn Rate消耗速度告警级别
1x正常消耗 28 天预算无需告警
6x6 天耗尽预算慢速告警
14.4x2 天耗尽预算快速告警
43.2x几小时耗尽预算紧急告警

第三步:告警策略

  • 多窗口告警:短窗口(5min)检测突发恶化 + 长窗口(1h/6h)检测持续劣化
  • 分级通知:warning → Slack 通知;critical → 电话 + 短信
  • 抑制噪声:告警恢复前不重复触发,维护窗口内静默
  • 关联上下文:告警附带部署信息、影响范围、历史趋势

相关链接前端性能优化 前端监控与错误追踪 前端埋点与数据分析