性能监测是前端性能优化的闭环环节——只有度量才能优化。本节介绍三种主流的性能监测工具,以及生产环境中的实战指标采集方法。
获取页面加载各阶段的时间戳:
// PerformanceTiming(传统 API,整个接口已废弃;推荐使用下方 Navigation Timing v2)
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.domLoading - timing.fetchStart, // 白屏时间(近似:文档开始渲染)
tti: timing.domInteractive - timing.fetchStart, // 首次可交互时间
loadTime: timing.loadEventEnd - timing.navigationStart, // 页面完全加载时间
};
console.log(metrics);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
// INP:基于 Event Timing API 的 event 条目(durationThreshold 之内的交互
// 带 interactionId,取所有交互中的最大值)。注意 first-input 条目对应的是
// 旧指标 FID,已被 INP 取代,不能直接当作 INP 使用。
vitalsObserver.observe({ type: "event", durationThreshold: 16, buffered: true });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, // 首字节时间
docFetchTime: entry.responseEnd - entry.fetchStart, // 文档获取耗时(FCP 应改用 paint timing 条目)
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`);
}
});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 是 Google 开源的自动化 Web 审计工具,可对页面性能、可访问性、SEO 等进行全面评估并给出优化建议。
方式一:Chrome DevTools 内置(推荐)
方式二:命令行
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;
}| 指标 | 全称 | 含义 | 目标值 |
|---|---|---|---|
| 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 s# 在 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)实现更完善的自动化流程。
Performance 面板的 Rendering → FPS meter 可开启帧率实时监控,辅助定位卡顿问题。
性能问题定位流程:
1. 看 FPS → 是否存在掉帧/卡顿?
2. 看 Summary → 哪类任务最耗时?(Script/Layout/Paint)
3. 看 Main 火焰图 → 具体是哪个函数?由什么事件触发?
4. 定位瓶颈 → 针对性优化Summary(饼图):按类型统计耗时占比。 Bottom-Up / Call Tree:从不同角度分析函数耗时。
Performance 面板的 NET 区域展示网络请求的瀑布图,可以分析:
开发环境用 DevTools/Lighthouse 即可,但生产环境需要持续的指标采集上报,才能了解真实用户环境下的性能。下面介绍一线大厂常用的实战采集方法。
SPA 页面无法基于 DOMContentLoaded 采集首屏——Vue/React 页面中,DOMContentLoaded 和 load 触发时页面展示的只是空白页,首屏此时还未渲染。必须使用 MutationObserver 采集。
核心思路:用 MutationObserver 监控 DOM 树变化,当 body 变化最剧烈(分数变化率最大)时对应的时间就是首屏时间。
步骤:
[时间点, 分数] 到数组。// 递归计算 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 首屏时间比较,取较大者为最终首屏时间。
一线大厂(阿里云、淘宝、阿里飞猪、得到、微店等)广泛使用该方案,兼容单页面应用和服务端模板页面。
白屏时间 = 页面开始展示时间点 - 开始请求时间点。借助 Performance API:FP = domLoading - navigationStart。
const whiteScreenTime = performance.timing.domLoading - performance.timing.navigationStart;App 场景:白屏时间多了 WebView 初始化时间(App 创建 WebView 到开始建立网络连接)。此时间需手动采集:在 App 测试版本中,创建 WebView 时打一个点,建立网络连接时打一个点,两点时间差即 WebView 初始化时间。
卡顿与否的关键在于单帧渲染耗时是否过长,而非平均 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 两个状态节点间的运行时长来判定。
网络环境是性能优化的盲区(尤其在 App 外部,如微信内页面、PC 站)。通过图片测速法间接判断:
知道了用户网络分布(如 50% 用户停留在 2G),就能针对性做弱网优化(如高清图用文本代替、仅展示购买按钮和价格等核心内容)。
生产环境中需要采集的关键性能指标:
// 采集 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;
}上报时机:
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 确保页面卸载时数据不丢失requestIdleCallback 在空闲时执行| 维度 | DevTools | Lighthouse | Performance API |
|---|---|---|---|
| 使用门槛 | 低(可视化) | 低(一键操作) | 中(需编程) |
| 灵活性 | 高(手动控制范围) | 中(固定审计项) | 最高(完全自定义) |
| 自动化 | 不支持 | 支持 CLI/API | 支持(需自行实现) |
| 生产环境 | 不适用 | 可定期跑 | 可持续上报 |
| 适合场景 | 开发调试 | 定期体检 | 生产监控 + 上报 |
domLoading - navigationStart、卡顿用 FPS 连续 3 帧 < 20、网络环境用图片测速法。