前端性能监控体系
What — 是什么
前端性能监控(Frontend Performance Monitoring)是一套持续采集、分析、可视化和告警页面性能指标的系统工程,涵盖从用户发起请求到页面可交互全链路的性能数据采集与洞察。它是前端工程化体系中不可或缺的一环,将”页面快不快”从主观感受转化为可量化的客观数据。
核心监控维度:
| 维度 | 说明 | 典型指标 |
|---|---|---|
| 加载指标 | 衡量页面资源加载速度 | TTFB、FCP、LCP |
| 交互指标 | 衡量用户操作响应速度 | FID、INP、TBT |
| 视觉指标 | 衡量页面视觉稳定性与呈现速度 | CLS、SI |
| 自定义指标 | 业务特定的性能度量 | 首屏业务数据可用时间、搜索结果渲染时间 |
核心概念
| 概念 | 说明 |
|---|---|
| Core Web Vitals | Google 定义的核心 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)包含:
- Core Web Vitals(LCP / INP / CLS)
- 移动端友好性
- 安全浏览(HTTPS)
- 无插页式广告
- HTTPS 安全性
这意味着性能差不仅影响现有用户,还会直接减少新用户的获取渠道。
需要数据驱动性能优化
没有监控的优化是盲目的:
- 不知道慢在哪里:用户反馈”页面卡”,但不知道是加载慢、渲染慢还是交互慢
- 不知道优化效果:做了优化,但无法量化提升幅度
- 不知道回归风险:新版本上线后性能劣化,无法及时发现
- 不知道优先级:多项优化待做,没有数据支撑决策先做哪个
监控方案对比
| 维度 | 自建监控 | Sentry | DataDog | 阿里云 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/height 或 aspect-ratio |
| 广告/嵌入 | 预留占位空间、使用 min-height |
| 动态内容 | 在视口下方插入内容、避免现有内容上移 |
| 字体加载 | font-display: swap 或 optional、size-adjust |
| SSR/SSG | 服务端渲染减少客户端动态注入 |
2. 其他 Web Vitals
| 指标 | 全称 | 含义 | 好的阈值 | 测量方式 |
|---|---|---|---|---|
| TTFB | Time to First Byte | 从请求发起到收到第一个字节的耗时 | ≤ 800ms | Navigation Timing |
| FCP | First Contentful Paint | 首次有内容绘制(文本、图片等) | ≤ 1.8s | Paint Timing |
| TBT | Total Blocking Time | FCP 到 TTI 之间所有长任务阻塞时间之和 | ≤ 200ms | Long Task API |
| SI | Speed Index | 页面视觉内容展现速度 | ≤ 3.4s | Lighthouse 合成 |
| TTI | Time to Interactive | 页面完全可交互时间 | ≤ 3.8s | Lighthouse 合成 |
// 采集 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% 的用户体验在此值以下 | 了解”大多数用户”的体验 |
| P75 | 75% 的用户体验在此值以下 | Google Search Console 使用 P75 |
| P90 | 90% 的用户体验在此值以下 | 识别较差体验的边界 |
| P95 | 95% 的用户体验在此值以下 | 性能告警常用阈值 |
| P99 | 99% 的用户体验在此值以下 | 极端情况分析 |
维度分析示例:
| 维度 | 分析价值 | 典型发现 |
|---|---|---|
| 地域 | 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. Core Web Vitals 包含哪三大指标?各自的评分标准是什么?
答:
Core Web Vitals 包含三大指标:
| 指标 | 含义 | 好的阈值 | 需改进 | 差 |
|---|---|---|---|---|
| LCP | 最大内容绘制,衡量加载性能 | ≤ 2.5s | 2.5~4.0s | > 4.0s |
| INP | 交互到下次绘制,衡量交互响应性 | ≤ 200ms | 200~500ms | > 500ms |
| CLS | 累积布局偏移,衡量视觉稳定性 | ≤ 0.1 | 0.1~0.25 | > 0.25 |
2024 年 3 月,INP 正式替代 FID 成为交互指标。三者分别覆盖”快不快”(加载)、“灵不灵”(交互)、“稳不稳”(视觉)三个维度。
2. LCP 优化可以从哪些方向入手?
答:
LCP 优化方向按影响链路从前往后排列:
- 服务端响应优化:CDN 缓存、Edge SSR/SSG、减少 TTFB
- 关键资源预加载:
<link rel="preload">加载 LCP 图片/字体,fetchpriority="high"提升优先级 - 渲染阻塞消除:关键 CSS 内联、非关键 CSS 延迟加载、JS 使用
async/defer - 图片优化:WebP/AVIF 格式、
srcset响应式尺寸、非首屏图片懒加载、CDN 图片处理 - 字体优化:
font-display: swap、<link rel="preload">字体、子集化 - 代码优化:Code Splitting 减少首屏 JS 体积、Tree Shaking 移除无用代码
核心思路:缩短 LCP 元素从请求到渲染的完整链路。
3. INP 和 FID 的区别是什么?为什么 INP 替代了 FID?
答:
| 维度 | FID | INP |
|---|---|---|
| 测量范围 | 仅首次交互 | 全页面生命周期所有交互 |
| 测量内容 | 仅输入延迟(从输入到事件回调开始) | 输入延迟 + 事件处理 + 呈现时间(完整延迟) |
| 评分阈值 | 好 ≤ 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() 的区别:
| 维度 | PerformanceObserver | performance.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);
}
优化策略:
-
任务拆分:将长任务拆为多个 < 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(); // 让出主线程,允许交互响应 } } -
Web Worker:计算密集型任务移至 Worker 线程
-
requestIdleCallback:非紧急任务在浏览器空闲时执行 -
虚拟列表:大数据列表分批渲染,避免一次性 DOM 操作
-
防抖节流:高频事件(scroll/resize)控制回调频率
7. P75 和 P95 分别是什么含义?为什么性能监控常用 P75 而非均值?
答:
- P75:75% 的数据小于此值。即 75% 的用户体验在此值以下,25% 的用户体验更差
- P95:95% 的数据小于此值。即 95% 的用户体验在此值以下,5% 的体验极差
为什么用 P75 而非均值:
- 不受极端值影响:性能数据常有极端值(后台页面、插件干扰等),均值会被拉偏,P75 更稳健
- 代表”大多数用户”体验:P75 表示 3/4 用户的体验水平,比均值更直觉
- Google 标准对齐:Google Search Console 使用 P75 评估 Core Web Vitals 达标情况
- SLO 制定依据:性能 SLO 通常设为”P75 LCP < 2.5s”而非”mean LCP < 2.5s”
分位值选择建议:
| 分位值 | 适用场景 |
|---|---|
| P50 | 了解典型体验 |
| P75 | SLO 目标、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 天预算 | 无需告警 |
| 6x | 6 天耗尽预算 | 慢速告警 |
| 14.4x | 2 天耗尽预算 | 快速告警 |
| 43.2x | 几小时耗尽预算 | 紧急告警 |
第三步:告警策略
- 多窗口告警:短窗口(5min)检测突发恶化 + 长窗口(1h/6h)检测持续劣化
- 分级通知:warning → Slack 通知;critical → 电话 + 短信
- 抑制噪声:告警恢复前不重复触发,维护窗口内静默
- 关联上下文:告警附带部署信息、影响范围、历史趋势