Skip to content

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 提供了页面导航过程中各个阶段的详细时间信息:

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 构建完整的性能监控体系

掌握这些工具,让你能够准确识别性能瓶颈,做出基于数据的优化决策,最终为用户提供流畅快速的使用体验。