性能监测工具与指标采集
性能监测是前端性能优化的闭环环节——只有度量才能优化。本节介绍三种主流的性能监测工具,以及生产环境中的实战指标采集方法。
一、浏览器 Performance API
1. Performance Timing API
获取页面加载各阶段的时间戳:
// PerformanceTiming(传统 API,部分字段已废弃)
const timing = performance.timing;
const metrics = {
dns: timing.domainLookupEnd - timing.domainLookupStart, // DNS 查询耗时
tcp: timing.connectEnd - timing.connectStart, // TCP 连接耗时
request: timing.responseEnd - timing.requestStart, // 请求响应耗时
domParse: timing.domComplete - timing.domLoading, // DOM 解析耗时
fpt: timing.responseEnd - timing.fetchStart, // 白屏时间(首次渲染)
tti: timing.domInteractive - timing.fetchStart, // 首次可交互时间
loadTime: timing.loadEventEnd - timing.navigationStart, // 页面完全加载时间
};
console.log(metrics);2. Performance Observer
Performance Observer 是现代观察者 API,可以异步监听各类性能条目的产生。
观察 Long Task(长时间运行的任务,>50ms):
if ("PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`Long Task detected: ${entry.duration}ms`);
}
});
observer.observe({ entryTypes: ["longtask"] });
}观察 Core Web Vitals:
const vitalsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.startTime, entry.value);
}
});
vitalsObserver.observe({ type: "largest-contentful-paint", buffered: true }); // LCP
vitalsObserver.observe({ type: "layout-shift", buffered: true }); // CLS
vitalsObserver.observe({ type: "first-input", buffered: true }); // INP(替代 FID)3. Performance Navigation Timing
Navigation Timing API v2(推荐,替代旧版 performance.timing):
performance.getEntriesByType("navigation").forEach((entry) => {
console.log({
dnsLookup: entry.domainLookupEnd - entry.domainLookupStart, // DNS
tcpConnect: entry.connectEnd - entry.connectStart, // TCP
tlsHandshake: entry.secureConnectionStart > 0
? entry.connectEnd - entry.secureConnectionStart
: 0, // TLS
ttfb: entry.responseStart - entry.requestStart, // 首字节时间
fcp: entry.responseEnd - entry.fetchStart, // 首次渲染(近似)
domContentLoaded: entry.domContentLoadedEventEnd - entry.fetchStart,
loadComplete: entry.loadEventEnd - entry.fetchStart,
});
});Resource Timing API(获取所有资源的加载时间):
performance.getEntriesByType("resource").forEach((resource) => {
if (resource.initiatorType === "img" || resource.initiatorType === "script") {
console.log(`${resource.name}: ${resource.duration.toFixed(0)}ms`);
}
});4. 自定义性能标记(mark & measure)
User Timing API 用于手动标记关键节点,测量业务逻辑耗时:
performance.mark("fetchData-start");
fetch("/api/data")
.then(() => performance.mark("fetchData-end"))
.then(() => performance.measure("api-fetch", "fetchData-start", "fetchData-end"))
.then(() => {
const measures = performance.getEntriesByName("api-fetch");
measures.forEach((m) => console.log(`API 耗时: ${m.duration}ms`));
});二、Lighthouse
Lighthouse 是 Google 开源的自动化 Web 审计工具,可对页面性能、可访问性、SEO 等进行全面评估并给出优化建议。
1. 安装与使用
方式一:Chrome DevTools 内置(推荐)
- 打开 DevTools → Lighthouse 面板(Chrome 60+)
- 选择分类(Performance 默认勾选)
- 点击 "Analyze page load"
- 等待报告生成
方式二:命令行
npm install -g lighthouse
lighthouse https://example.com --view方式三:Node.js API
const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
async function audit(url) {
const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless"] });
const options = { logLevel: "info", output: "html", port: chrome.port };
const results = await lighthouse(url, options);
await chrome.kill();
return results;
}2. 核心指标解读
| 指标 | 全称 | 含义 | 目标值 |
|---|---|---|---|
| FCP | First Contentful Paint | 首次内容绘制时间 | < 1.8s |
| LCP | Largest Contentful Paint | 最大内容绘制时间 | < 2.5s |
| TBT | Total Blocking Time | 总阻塞时间 | < 200ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | < 0.1 |
| SI | Speed Index | 速度指数 | < 3.4s |
| TTI | Time to Interactive | 可交互时间 | < 3.8s |
Lighthouse 的 "Opportunities" 部分会给出具体的优化建议及预期收益:
建议 预期节省
───────────────────────── ────────
启用文本压缩 0.5 s
使用高效的缓存策略 0.3 s
减少 JavaScript 执行时间 1.2 s
适当调整图片大小 0.8 s
消除阻塞渲染的资源 0.4 s3. CI 集成
# 在 CI 中运行 Lighthouse 并设置性能预算
lighthouse https://staging.example.com \
--output=json \
--output-path=./lighthouse-report.json \
--chrome-flags="--headless"// Node.js 脚本:检查性能分数是否达标
const results = await audit("https://staging.example.com");
const performanceScore = results.lhr.categories.performance.score * 100;
if (performanceScore < 90) {
console.error(`Performance score ${performanceScore} is below threshold 90`);
process.exit(1);
}CI 集成要点:在预发布环境(staging)运行;设置性能分数阈值,低于则构建失败;将报告上传到存储服务便于历史对比;结合 Lighthouse CI(@lhci/cli)实现更完善的自动化流程。
三、Chrome DevTools Performance 面板
1. FPS 监控
Performance 面板的 Rendering → FPS meter 可开启帧率实时监控,辅助定位卡顿问题。
性能问题定位流程:
1. 看 FPS → 是否存在掉帧/卡顿?
2. 看 Summary → 哪类任务最耗时?(Script/Layout/Paint)
3. 看 Main 火焰图 → 具体是哪个函数?由什么事件触发?
4. 定位瓶颈 → 针对性优化Summary(饼图):按类型统计耗时占比。 Bottom-Up / Call Tree:从不同角度分析函数耗时。
2. 网络瀑布图
Performance 面板的 NET 区域展示网络请求的瀑布图,可以分析:
- 资源加载的先后顺序和依赖关系
- 各请求的耗时分布(DNS、TCP、TLS、等待、下载)
- 关键路径上的阻塞资源
- 是否存在不必要的串行请求
四、生产环境实战指标采集
开发环境用 DevTools/Lighthouse 即可,但生产环境需要持续的指标采集上报,才能了解真实用户环境下的性能。下面介绍一线大厂常用的实战采集方法。
1. 首屏时间采集
SPA 页面无法基于 DOMContentLoaded 采集首屏——Vue/React 页面中,DOMContentLoaded 和 load 触发时页面展示的只是空白页,首屏此时还未渲染。必须使用 MutationObserver 采集。
核心思路:用 MutationObserver 监控 DOM 树变化,当 body 变化最剧烈(分数变化率最大)时对应的时间就是首屏时间。
步骤:
- 用户进入页面时用 MutationObserver 监控 DOM 元素变化。
- DOM 变化时记录
[时间点, 分数]到数组。 - 递归遍历 DOM 计算分数:按元素层级设权重(第一层权重 1,每层 +0.5),渲染时累加分数。
- 根据分数计算变化率,变化率最大点对应的时间即首屏时间。
- 若页面含图片,还需对比图片加载完成时间,取较大者。
// 递归计算 DOM 分数(按层级加权)
function calculateScore(el, tiers, parentScore) {
let score = 0;
const tagName = el.tagName;
// 排除无用标签
if ("SCRIPT" !== tagName && "STYLE" !== tagName && "META" !== tagName && "HEAD" !== tagName) {
const childrenLen = el.children ? el.children.length : 0;
if (childrenLen > 0) {
for (let i = childrenLen - 1; i >= 0; i--) {
score += calculateScore(el.children[i], tiers + 1, score > 0);
}
}
if (score <= 0 && !parentScore) {
if (!(el.getBoundingClientRect && el.getBoundingClientRect().top < innerHeight)) {
return 0; // 超出可视区域
}
}
score += 1 + 0.5 * tiers; // 层级越深权重越大
}
return score;
}终止条件:为避免一直采集,设置首屏采集终止条件——计算时间超过 30 秒;计算 4 轮且 1 秒内分数不再变化;计算 9 次且分数不再变化。
图片处理:图片加载是异步的,容器(DOM 元素)和内容(图片)加载分离。需遍历所有图片路径,用 performance.getEntriesByName(src)[0].responseEnd 获取图片下载完成时间,与 DOM 首屏时间比较,取较大者为最终首屏时间。
一线大厂(阿里云、淘宝、阿里飞猪、得到、微店等)广泛使用该方案,兼容单页面应用和服务端模板页面。
2. 白屏时间采集
白屏时间 = 页面开始展示时间点 - 开始请求时间点。借助 Performance API:FP = domLoading - navigationStart。
const whiteScreenTime = performance.timing.domLoading - performance.timing.navigationStart;App 场景:白屏时间多了 WebView 初始化时间(App 创建 WebView 到开始建立网络连接)。此时间需手动采集:在 App 测试版本中,创建 WebView 时打一个点,建立网络连接时打一个点,两点时间差即 WebView 初始化时间。
3. 卡顿(FPS)采集
卡顿与否的关键在于单帧渲染耗时是否过长,而非平均 FPS。浏览器拿不到单帧渲染耗时接口,只能通过 FPS 计算:连续 3 帧不低于 20 FPS,且保持恒定,则认为流畅。
利用 requestAnimationFrame 每秒执行 60 次(不卡顿时)的特性计算 FPS:
const rAF = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function (callback) { window.setTimeout(callback, 1000 / 60); }; // 兼容
const config = { lastTime: performance.now(), lastFrameTime: performance.now(), frame: 0 };
function fpsLoop() {
const now = performance.now();
const diff = now - config.lastFrameTime;
config.lastFrameTime = now;
config.frame++;
if (now > 1000 + config.lastTime) {
const fps = Math.round((config.frame * 1000) / (now - config.lastTime));
console.log(`FPS: ${fps}`);
config.frame = 0;
config.lastTime = now;
}
rAF(fpsLoop);
}
// 判断是否卡顿:连续 3 帧 FPS 低于 20
function isBlocking(fpsList, below = 20, last = 3) {
let count = 0;
for (let i = 0; i < fpsList.length; i++) {
if (fpsList[i] && fpsList[i] < below) {
count++;
} else {
count = 0;
}
if (count >= last) return true;
}
return false;
}
fpsLoop();App 侧采集:App 可直接获取单帧渲染时长。Android 通过 mChoreographer.getFrameTimeNanos() 与 System.nanoTime() 差值计算,单帧超过 250ms 判定严重卡顿、连续 5 次超过 50ms 判定卡顿;iOS 通过 CFRunLoop 监听主线程 BeforeSources 和 AfterWaiting 两个状态节点间的运行时长来判定。
4. 网络环境采集
网络环境是性能优化的盲区(尤其在 App 外部,如微信内页面、PC 站)。通过图片测速法间接判断:
- 请求两张不同尺寸的图片(如 1×1 和 3×3 像素)。
- 记录图片请求开始和 onLoad 完成的时间点,差值为图片加载时间。
- 用文件体积除以加载时间得到加载速度,求两张图片速度的平均值作为网络速度。
- 把每次页面启动采集的网络速度做概率分布,判定网络环境(2G:750-1400ms、3G:230-750ms、4G/WiFi:0-230ms)。
知道了用户网络分布(如 50% 用户停留在 2G),就能针对性做弱网优化(如高清图用文本代替、仅展示购买按钮和价格等核心内容)。
五、性能数据上报
1. 指标采集
生产环境中需要采集的关键性能指标:
// 采集 Core Web Vitals
function collectWebVitals() {
const vitals = {};
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
vitals.lcp = lastEntry.renderTime || lastEntry.loadTime;
}).observe({ type: "largest-contentful-paint", buffered: true });
// Navigation Timing
const nav = performance.getEntriesByType("navigation")[0];
if (nav) {
vitals.domContentLoaded = nav.domContentLoadedEventEnd - nav.fetchStart;
vitals.loadComplete = nav.loadEventEnd - nav.fetchStart;
}
return vitals;
}2. 上报策略
上报时机:
- 页面加载完成时上报 Navigation Timing 数据
- LCP/CLS 等指标在值稳定后上报(LCP 在用户交互或页面隐藏后最终确定)
- 长任务(Long Task)实时监听并上报
- 页面卸载(
visibilitychange)时上报兜底数据
上报方式:
// 使用 sendBeacon 上报(页面卸载时也能可靠发送)
function reportMetrics(data) {
const payload = JSON.stringify({
...data,
url: location.href,
timestamp: Date.now(),
});
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/performance", payload);
} else {
// 降级使用 Image 请求
new Image().src = `/api/performance?data=${encodeURIComponent(payload)}`;
}
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
reportMetrics(collectWebVitals());
}
});上报优化建议:
- 使用
navigator.sendBeacon确保页面卸载时数据不丢失 - 合并多条指标为一次上报请求,减少网络开销
- 采样上报:对高流量页面按比例采样(如 10%),避免上报接口过载
- 数据压缩:上报数据使用 gzip 压缩或精简字段名
- 异步上报:不阻塞主线程,使用
requestIdleCallback在空闲时执行 - 过滤脏数据:丢弃明显异常的数据(如本地调试产生的超长加载时间),避免占用带宽
六、三种监测方案对比
| 维度 | DevTools | Lighthouse | Performance API |
|---|---|---|---|
| 使用门槛 | 低(可视化) | 低(一键操作) | 中(需编程) |
| 灵活性 | 高(手动控制范围) | 中(固定审计项) | 最高(完全自定义) |
| 自动化 | 不支持 | 支持 CLI/API | 支持(需自行实现) |
| 生产环境 | 不适用 | 可定期跑 | 可持续上报 |
| 适合场景 | 开发调试 | 定期体检 | 生产监控 + 上报 |
总结
- 开发调试用 DevTools,定期体检用 Lighthouse,生产监控用 Performance API + 上报。
- 生产环境实战采集:首屏用 MutationObserver(SPA)、白屏用
domLoading - navigationStart、卡顿用 FPS 连续 3 帧 < 20、网络环境用图片测速法。 - 采集与上报是性能监控闭环的起点,为后续的监控预警平台提供数据基础。