{T}

性能监测工具与指标采集

性能监测是前端性能优化的闭环环节——只有度量才能优化。本节介绍三种主流的性能监测工具,以及生产环境中的实战指标采集方法。

一、浏览器 Performance API

1. Performance Timing API

获取页面加载各阶段的时间戳:

javascript
// 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)

javascript
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

javascript
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):

javascript
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(获取所有资源的加载时间):

javascript
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 用于手动标记关键节点,测量业务逻辑耗时:

javascript
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 内置(推荐)

  1. 打开 DevTools → Lighthouse 面板(Chrome 60+)
  2. 选择分类(Performance 默认勾选)
  3. 点击 "Analyze page load"
  4. 等待报告生成

方式二:命令行

bash
npm install -g lighthouse
lighthouse https://example.com --view

方式三:Node.js API

javascript
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. 核心指标解读

指标全称含义目标值
FCPFirst Contentful Paint首次内容绘制时间< 1.8s
LCPLargest Contentful Paint最大内容绘制时间< 2.5s
TBTTotal Blocking Time总阻塞时间< 200ms
CLSCumulative Layout Shift累积布局偏移< 0.1
SISpeed Index速度指数< 3.4s
TTITime to Interactive可交互时间< 3.8s

Lighthouse 的 "Opportunities" 部分会给出具体的优化建议及预期收益:

code
建议                          预期节省
─────────────────────────    ────────
启用文本压缩                    0.5 s
使用高效的缓存策略               0.3 s
减少 JavaScript 执行时间        1.2 s
适当调整图片大小                 0.8 s
消除阻塞渲染的资源               0.4 s

3. CI 集成

bash
# 在 CI 中运行 Lighthouse 并设置性能预算
lighthouse https://staging.example.com \
  --output=json \
  --output-path=./lighthouse-report.json \
  --chrome-flags="--headless"
javascript
// 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 可开启帧率实时监控,辅助定位卡顿问题。

性能问题定位流程

code
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 变化最剧烈(分数变化率最大)时对应的时间就是首屏时间。

步骤

  1. 用户进入页面时用 MutationObserver 监控 DOM 元素变化。
  2. DOM 变化时记录 [时间点, 分数] 到数组。
  3. 递归遍历 DOM 计算分数:按元素层级设权重(第一层权重 1,每层 +0.5),渲染时累加分数。
  4. 根据分数计算变化率,变化率最大点对应的时间即首屏时间。
  5. 若页面含图片,还需对比图片加载完成时间,取较大者。
javascript
// 递归计算 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

javascript
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:

javascript
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 监听主线程 BeforeSourcesAfterWaiting 两个状态节点间的运行时长来判定。

4. 网络环境采集

网络环境是性能优化的盲区(尤其在 App 外部,如微信内页面、PC 站)。通过图片测速法间接判断:

  1. 请求两张不同尺寸的图片(如 1×1 和 3×3 像素)。
  2. 记录图片请求开始和 onLoad 完成的时间点,差值为图片加载时间。
  3. 用文件体积除以加载时间得到加载速度,求两张图片速度的平均值作为网络速度。
  4. 把每次页面启动采集的网络速度做概率分布,判定网络环境(2G:750-1400ms、3G:230-750ms、4G/WiFi:0-230ms)。

知道了用户网络分布(如 50% 用户停留在 2G),就能针对性做弱网优化(如高清图用文本代替、仅展示购买按钮和价格等核心内容)。

五、性能数据上报

1. 指标采集

生产环境中需要采集的关键性能指标:

javascript
// 采集 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)时上报兜底数据

上报方式

javascript
// 使用 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 在空闲时执行
  • 过滤脏数据:丢弃明显异常的数据(如本地调试产生的超长加载时间),避免占用带宽

六、三种监测方案对比

维度DevToolsLighthousePerformance API
使用门槛低(可视化)低(一键操作)中(需编程)
灵活性高(手动控制范围)中(固定审计项)最高(完全自定义)
自动化不支持支持 CLI/API支持(需自行实现)
生产环境不适用可定期跑可持续上报
适合场景开发调试定期体检生产监控 + 上报

总结

  • 开发调试用 DevTools,定期体检用 Lighthouse,生产监控用 Performance API + 上报
  • 生产环境实战采集:首屏用 MutationObserver(SPA)、白屏用 domLoading - navigationStart、卡顿用 FPS 连续 3 帧 < 20、网络环境用图片测速法
  • 采集与上报是性能监控闭环的起点,为后续的监控预警平台提供数据基础。