Performance API:Web 性能优化的精密仪表盘
为什么需要 Performance API
用户不喜欢等待。研究表明,页面加载时间每增加一秒,用户流失率就会显著上升。但在优化性能之前,你需要先能够准确地测量它——这就是 Performance API 存在的意义。
Date.now() 虽然能告诉你时间,但它的精度只有毫秒级,而且容易受到系统时钟调整的影响。Performance API 提供了微秒级的高精度计时,以及丰富的性能指标采集能力,让你能够像医生看 X 光片一样,清晰地看到你的 Web 应用在性能方面的"骨骼结构"。
Performance 对象概览
performance 对象是 Performance API 的入口,它挂载在 window 对象上:
javascript
// 获取 performance 对象
const perf = window.performance;
// 查看可用的 API
console.log(
"Performance API:",
Object.getOwnPropertyNames(Performance.prototype)
);
// 基础属性
console.log("时间原点:", performance.timeOrigin);
// 页面导航开始的绝对时间戳(Unix 时间)
console.log("当前时间:", performance.now());
// 相对于 timeOrigin 的高精度时间高精度计时:performance.now()
performance.now() 返回从页面导航开始到现在的毫秒数,但精度可达微秒级(受安全限制):
javascript
// 基础用法
const startTime = performance.now();
// 执行一些操作
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
const endTime = performance.now();
console.log(`执行时间: ${endTime - startTime}ms`);
// 输出类似: 执行时间: 12.399999998509884ms
// 对比 Date.now()
const dateStart = Date.now();
const perfStart = performance.now();
// Date.now() 精度只有毫秒
console.log("Date.now():", dateStart); // 1703841234567
console.log("performance.now():", perfStart); // 1234.567890为什么更精确
javascript
// performance.now() 的优势:
// 1. 不受系统时钟调整影响
// 2. 单调递增,不会出现时间倒流
// 3. 微秒级精度(虽然浏览器可能会降低精度以防止特定攻击)
function measureExecution(fn, iterations = 1000) {
const times = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
const end = performance.now();
times.push(end - start);
}
// 计算统计数据
const sum = times.reduce((a, b) => a + b, 0);
const avg = sum / times.length;
const sorted = [...times].sort((a, b) => a - b);
const median = sorted[Math.floor(times.length / 2)];
const min = sorted[0];
const max = sorted[sorted.length - 1];
return { avg, median, min, max };
}
// 使用示例
const stringConcatStats = measureExecution(() => {
let str = "";
for (let i = 0; i < 100; i++) {
str += "a";
}
});
console.log("字符串拼接性能:", stringConcatStats);性能标记与测量
performance.mark() - 打标记
mark() 方法可以在性能时间线上创建标记点:
javascript
// 创建标记
performance.mark("app-start");
// 初始化应用...
initializeApp();
performance.mark("app-initialized");
// 加载数据...
await loadData();
performance.mark("data-loaded");
// 渲染 UI...
renderUI();
performance.mark("ui-rendered");
// 查看所有标记
const marks = performance.getEntriesByType("mark");
console.table(
marks.map((m) => ({
name: m.name,
startTime: m.startTime.toFixed(2) + "ms",
}))
);performance.measure() - 测量区间
measure() 方法可以测量两个标记之间的时间:
javascript
// 基于两个标记创建测量
performance.mark("fetch-start");
await fetch("/api/data");
performance.mark("fetch-end");
performance.measure("API 调用", "fetch-start", "fetch-end");
// 获取测量结果
const measures = performance.getEntriesByType("measure");
measures.forEach((measure) => {
console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
});
// 也可以测量从页面开始到某个标记的时间
performance.mark("first-paint-complete");
performance.measure("首次绘制时间", undefined, "first-paint-complete");
// 或从某个标记到当前时间
performance.mark("load-start");
// ...
performance.measure("加载进行中", "load-start");清理标记和测量
javascript
// 清除特定标记
performance.clearMarks("app-start");
// 清除所有标记
performance.clearMarks();
// 清除特定测量
performance.clearMeasures("API 调用");
// 清除所有测量
performance.clearMeasures();实际应用:性能追踪器
javascript
class PerformanceTracker {
constructor(prefix = "perf") {
this.prefix = prefix;
this.spans = new Map();
}
// 开始一个跨度
startSpan(name) {
const markName = `${this.prefix}:${name}:start`;
performance.mark(markName);
this.spans.set(name, markName);
return markName;
}
// 结束一个跨度
endSpan(name) {
const startMark = this.spans.get(name);
if (!startMark) {
console.warn(`未找到跨度: ${name}`);
return null;
}
const endMark = `${this.prefix}:${name}:end`;
const measureName = `${this.prefix}:${name}`;
performance.mark(endMark);
performance.measure(measureName, startMark, endMark);
this.spans.delete(name);
const entries = performance.getEntriesByName(measureName, "measure");
return entries[entries.length - 1]?.duration;
}
// 测量函数执行时间
async measureAsync(name, fn) {
this.startSpan(name);
try {
return await fn();
} finally {
const duration = this.endSpan(name);
console.log(`[${name}] 耗时: ${duration?.toFixed(2)}ms`);
}
}
// 同步版本
measureSync(name, fn) {
this.startSpan(name);
try {
return fn();
} finally {
const duration = this.endSpan(name);
console.log(`[${name}] 耗时: ${duration?.toFixed(2)}ms`);
}
}
// 获取所有测量结果
getAllMeasures() {
return performance
.getEntriesByType("measure")
.filter((m) => m.name.startsWith(this.prefix))
.map((m) => ({
name: m.name.replace(`${this.prefix}:`, ""),
duration: m.duration,
}));
}
// 获取报告
getReport() {
const measures = this.getAllMeasures();
const report = {};
measures.forEach((m) => {
if (!report[m.name]) {
report[m.name] = {
count: 0,
total: 0,
min: Infinity,
max: -Infinity,
};
}
report[m.name].count++;
report[m.name].total += m.duration;
report[m.name].min = Math.min(report[m.name].min, m.duration);
report[m.name].max = Math.max(report[m.name].max, m.duration);
});
// 计算平均值
Object.values(report).forEach((r) => {
r.avg = r.total / r.count;
});
return report;
}
// 清理
clear() {
performance.clearMarks();
performance.clearMeasures();
this.spans.clear();
}
}
// 使用示例
const tracker = new PerformanceTracker("myApp");
// 追踪 API 调用
await tracker.measureAsync("fetchUsers", async () => {
const response = await fetch("/api/users");
return response.json();
});
// 追踪渲染
tracker.measureSync("renderList", () => {
// 渲染逻辑
});
// 获取报告
console.table(tracker.getReport());Navigation Timing API
Navigation Timing 提供了页面导航过程中各个阶段的详细时间信息:
javascript
// 获取导航时间数据
const navEntry = performance.getEntriesByType("navigation")[0];
if (navEntry) {
console.log("导航类型:", navEntry.type);
// 'navigate', 'reload', 'back_forward', 'prerender'
// 关键时间点
console.log(
"DNS 查询时间:",
navEntry.domainLookupEnd - navEntry.domainLookupStart
);
console.log("TCP 连接时间:", navEntry.connectEnd - navEntry.connectStart);
console.log("请求时间:", navEntry.responseStart - navEntry.requestStart);
console.log("响应时间:", navEntry.responseEnd - navEntry.responseStart);
console.log(
"DOM 解析时间:",
navEntry.domContentLoadedEventEnd - navEntry.responseEnd
);
console.log("页面加载总时间:", navEntry.loadEventEnd - navEntry.startTime);
}
// 或使用旧版 API(兼容性更好)
const timing = performance.timing;
if (timing) {
console.log("页面加载时间:", timing.loadEventEnd - timing.navigationStart);
console.log(
"DOM 就绪时间:",
timing.domContentLoadedEventEnd - timing.navigationStart
);
}页面加载指标可视化
javascript
function visualizePageLoadTiming() {
const nav = performance.getEntriesByType("navigation")[0];
if (!nav) {
console.log("导航时间数据不可用");
return;
}
const timeline = [
{
phase: "DNS 查询",
start: nav.domainLookupStart,
end: nav.domainLookupEnd,
},
{ phase: "TCP 连接", start: nav.connectStart, end: nav.connectEnd },
{
phase: "SSL 握手",
start: nav.secureConnectionStart || nav.connectStart,
end: nav.connectEnd,
},
{ phase: "请求发送", start: nav.requestStart, end: nav.responseStart },
{ phase: "响应接收", start: nav.responseStart, end: nav.responseEnd },
{ phase: "DOM 解析", start: nav.responseEnd, end: nav.domInteractive },
{
phase: "DOMContentLoaded",
start: nav.domContentLoadedEventStart,
end: nav.domContentLoadedEventEnd,
},
{ phase: "Load 事件", start: nav.loadEventStart, end: nav.loadEventEnd },
];
console.log("\n📊 页面加载时间线:");
console.log("=".repeat(60));
timeline.forEach(({ phase, start, end }) => {
const duration = end - start;
if (duration > 0) {
const bar = "█".repeat(Math.min(Math.round(duration / 10), 40));
console.log(`${phase.padEnd(18)} ${bar} ${duration.toFixed(2)}ms`);
}
});
console.log("=".repeat(60));
console.log(`总加载时间: ${nav.loadEventEnd.toFixed(2)}ms`);
}
// 页面加载完成后执行
window.addEventListener("load", () => {
setTimeout(visualizePageLoadTiming, 0);
});Resource Timing API
Resource Timing 提供了每个资源加载的详细时间信息:
javascript
// 获取所有资源加载时间
const resources = performance.getEntriesByType("resource");
console.log(`共加载 ${resources.length} 个资源`);
resources.forEach((resource) => {
console.log({
name: resource.name,
type: resource.initiatorType,
duration: resource.duration.toFixed(2) + "ms",
size: resource.transferSize + " bytes",
});
});
// 按类型分组统计
function analyzeResourcesByType() {
const resources = performance.getEntriesByType("resource");
const byType = {};
resources.forEach((r) => {
if (!byType[r.initiatorType]) {
byType[r.initiatorType] = {
count: 0,
totalDuration: 0,
totalSize: 0,
};
}
byType[r.initiatorType].count++;
byType[r.initiatorType].totalDuration += r.duration;
byType[r.initiatorType].totalSize += r.transferSize || 0;
});
return byType;
}
console.table(analyzeResourcesByType());找出慢资源
javascript
function findSlowResources(threshold = 500) {
const resources = performance.getEntriesByType("resource");
const slowResources = resources
.filter((r) => r.duration > threshold)
.sort((a, b) => b.duration - a.duration)
.map((r) => ({
url: r.name.split("/").pop(), // 只显示文件名
fullUrl: r.name,
type: r.initiatorType,
duration: r.duration.toFixed(2) + "ms",
size: (r.transferSize / 1024).toFixed(2) + "KB",
}));
if (slowResources.length > 0) {
console.warn(
`⚠️ 发现 ${slowResources.length} 个慢资源(>${threshold}ms):`
);
console.table(slowResources);
} else {
console.log(`✓ 所有资源加载时间都在 ${threshold}ms 以内`);
}
return slowResources;
}
// 页面加载完成后检查
window.addEventListener("load", () => {
setTimeout(() => findSlowResources(300), 100);
});First Paint 和 Largest Contentful Paint
现代浏览器提供了 Paint Timing API 来测量关键渲染指标:
javascript
// 获取绘制时间
function getPaintTimings() {
const paintEntries = performance.getEntriesByType("paint");
const timings = {};
paintEntries.forEach((entry) => {
timings[entry.name] = entry.startTime;
});
console.log("First Paint:", timings["first-paint"]?.toFixed(2) + "ms");
console.log(
"First Contentful Paint:",
timings["first-contentful-paint"]?.toFixed(2) + "ms"
);
return timings;
}
// 使用 PerformanceObserver 监听 LCP
function observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(
"Largest Contentful Paint:",
lastEntry.startTime.toFixed(2) + "ms"
);
console.log("LCP 元素:", lastEntry.element);
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
}
// 页面加载后检查
window.addEventListener("load", () => {
getPaintTimings();
observeLCP();
});PerformanceObserver
PerformanceObserver 可以异步观察性能条目的创建:
javascript
// 观察所有性能条目
const observer = new PerformanceObserver((list, observer) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(
`[${entry.entryType}] ${entry.name}: ${
entry.duration?.toFixed(2) || entry.startTime.toFixed(2)
}ms`
);
});
});
// 观察多种类型
observer.observe({
entryTypes: ["mark", "measure", "resource", "navigation"],
});
// 只观察特定类型(更高效)
const resourceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 1000) {
console.warn(`慢资源: ${entry.name} (${entry.duration.toFixed(0)}ms)`);
}
});
});
resourceObserver.observe({ type: "resource", buffered: true });
// 停止观察
// observer.disconnect();监控 Long Tasks
javascript
// 监控长任务(超过 50ms 的任务)
function observeLongTasks() {
if (!PerformanceObserver.supportedEntryTypes.includes("longtask")) {
console.log("浏览器不支持 Long Task 监控");
return;
}
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.warn("⚠️ 检测到长任务:", {
duration: entry.duration.toFixed(2) + "ms",
startTime: entry.startTime.toFixed(2) + "ms",
name: entry.name,
});
});
});
observer.observe({ type: "longtask", buffered: true });
return observer;
}
observeLongTasks();性能监控系统
结合以上 API,构建一个完整的性能监控系统:
javascript
class PerformanceMonitor {
constructor(options = {}) {
this.metrics = {};
this.options = {
reportUrl: options.reportUrl,
sampleRate: options.sampleRate || 1,
...options,
};
this.init();
}
init() {
// 采样判断
if (Math.random() > this.options.sampleRate) {
return;
}
this.collectNavigationTiming();
this.collectPaintTiming();
this.observeResources();
this.observeLongTasks();
this.collectWebVitals();
// 页面卸载时发送数据
window.addEventListener("beforeunload", () => {
this.sendReport();
});
// 也可以定期发送
// setInterval(() => this.sendReport(), 60000);
}
collectNavigationTiming() {
window.addEventListener("load", () => {
setTimeout(() => {
const nav = performance.getEntriesByType("navigation")[0];
if (nav) {
this.metrics.navigation = {
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
ttfb: nav.responseStart - nav.requestStart,
download: nav.responseEnd - nav.responseStart,
domParse: nav.domInteractive - nav.responseEnd,
domReady: nav.domContentLoadedEventEnd - nav.startTime,
load: nav.loadEventEnd - nav.startTime,
};
}
}, 0);
});
}
collectPaintTiming() {
window.addEventListener("load", () => {
setTimeout(() => {
const paints = performance.getEntriesByType("paint");
paints.forEach((paint) => {
this.metrics[paint.name] = paint.startTime;
});
}, 0);
});
}
observeResources() {
const observer = new PerformanceObserver((list) => {
if (!this.metrics.resources) {
this.metrics.resources = {
count: 0,
totalSize: 0,
totalDuration: 0,
slow: [],
};
}
list.getEntries().forEach((entry) => {
this.metrics.resources.count++;
this.metrics.resources.totalSize += entry.transferSize || 0;
this.metrics.resources.totalDuration += entry.duration;
if (entry.duration > 1000) {
this.metrics.resources.slow.push({
url: entry.name,
duration: entry.duration,
size: entry.transferSize,
});
}
});
});
observer.observe({ type: "resource", buffered: true });
}
observeLongTasks() {
if (!PerformanceObserver.supportedEntryTypes.includes("longtask")) {
return;
}
this.metrics.longTasks = [];
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.metrics.longTasks.push({
startTime: entry.startTime,
duration: entry.duration,
});
});
});
observer.observe({ type: "longtask", buffered: true });
}
collectWebVitals() {
// LCP
if (
PerformanceObserver.supportedEntryTypes.includes(
"largest-contentful-paint"
)
) {
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
this.metrics.lcp = entries[entries.length - 1]?.startTime;
});
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
}
// FID (需要用户交互才能触发)
if (PerformanceObserver.supportedEntryTypes.includes("first-input")) {
const fidObserver = new PerformanceObserver((list) => {
const entry = list.getEntries()[0];
this.metrics.fid = entry.processingStart - entry.startTime;
});
fidObserver.observe({ type: "first-input", buffered: true });
}
// CLS
if (PerformanceObserver.supportedEntryTypes.includes("layout-shift")) {
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
this.metrics.cls = clsValue;
});
clsObserver.observe({ type: "layout-shift", buffered: true });
}
}
getReport() {
return {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
metrics: this.metrics,
};
}
sendReport() {
const report = this.getReport();
if (this.options.reportUrl) {
// 使用 sendBeacon 确保数据发送
const blob = new Blob([JSON.stringify(report)], {
type: "application/json",
});
navigator.sendBeacon(this.options.reportUrl, blob);
}
console.log("📊 性能报告:", report);
return report;
}
}
// 使用示例
const monitor = new PerformanceMonitor({
reportUrl: "/api/performance",
sampleRate: 0.1, // 10% 采样率
});
// 手动获取报告
setTimeout(() => {
console.table(monitor.getReport().metrics);
}, 5000);性能优化实践
识别性能瓶颈
javascript
function diagnosePerformance() {
const nav = performance.getEntriesByType("navigation")[0];
const issues = [];
// 检查 DNS
if (nav.domainLookupEnd - nav.domainLookupStart > 100) {
issues.push({
type: "DNS",
message: "DNS 查询时间过长,考虑使用 DNS 预解析",
solution: '<link rel="dns-prefetch" href="//your-domain.com">',
});
}
// 检查 TTFB
if (nav.responseStart - nav.requestStart > 500) {
issues.push({
type: "TTFB",
message: "TTFB 过长,服务器响应慢",
solution: "优化服务器端处理,使用 CDN,启用缓存",
});
}
// 检查资源大小
const resources = performance.getEntriesByType("resource");
const largeResources = resources.filter(
(r) => (r.transferSize || 0) > 500 * 1024
);
if (largeResources.length > 0) {
issues.push({
type: "RESOURCE_SIZE",
message: `${largeResources.length} 个资源超过 500KB`,
solution: "压缩资源,使用图片优化,按需加载",
details: largeResources.map((r) => r.name),
});
}
// 检查资源数量
if (resources.length > 100) {
issues.push({
type: "RESOURCE_COUNT",
message: `资源请求数过多 (${resources.length})`,
solution: "合并资源,使用 HTTP/2,懒加载",
});
}
return issues;
}
// 运行诊断
window.addEventListener("load", () => {
setTimeout(() => {
const issues = diagnosePerformance();
if (issues.length > 0) {
console.warn("🔍 发现性能问题:");
issues.forEach((issue) => {
console.group(`❌ ${issue.type}`);
console.log("问题:", issue.message);
console.log("建议:", issue.solution);
if (issue.details) {
console.log("详情:", issue.details);
}
console.groupEnd();
});
} else {
console.log("✅ 性能检查通过!");
}
}, 1000);
});总结
Performance API 是构建高性能 Web 应用的重要工具。它提供了从粗粒度的页面加载时间到细粒度的资源加载分析、从简单的代码执行计时到复杂的 Web Vitals 指标收集的全方位能力。
关键要点:
performance.now()提供高精度计时mark()和measure()用于自定义性能标记- Navigation Timing 分析页面加载各阶段
- Resource Timing 追踪每个资源的加载性能
- PerformanceObserver 实现异步性能监控
- 结合 Web Vitals 构建完整的性能监控体系
掌握这些工具,让你能够准确识别性能瓶颈,做出基于数据的优化决策,最终为用户提供流畅快速的使用体验。