前端性能指标与测量(Performance API)

前端性能不能只看“页面什么时候加载完”。用户更关心的是:服务器什么时候响应、页面什么时候出现内容、主体内容什么时候可见、页面什么时候可以流畅交互,以及加载过程中是否发生跳动。

1. Core Web Vitals

Core Web Vitals 是 Google 用来衡量真实用户体验的一组核心指标。当前包括:

  1. LCP(Largest Contentful Paint):最大内容绘制,衡量加载性能
    • 良好:≤ 2.5s
    • 表示视口内最大的图片、文本块或视频封面等候选元素完成绘制的时间。
  2. INP(Interaction to Next Paint):交互到下次绘制,衡量交互响应性
    • 良好:≤ 200ms
    • 观察点击、触摸和键盘输入等交互,并用接近最慢的一次交互延迟代表页面响应性。
    • INP 已经替代只关注首次交互的 FID。
  3. CLS(Cumulative Layout Shift):累积布局偏移,衡量视觉稳定性
    • 良好:≤ 0.1
    • 例如图片没有预留尺寸,加载后把下方内容突然推开,就可能产生布局偏移。

评估线上数据时,通常看页面访问数据的 第 75 百分位(P75) 是否达到“良好”标准。

Core Web Vitals 不等于首屏加载指标。LCP 与首屏加载直接相关;INP 关注交互响应;CLS 统计页面生命周期中的意外布局偏移。

2. 首屏加载还要关注哪些指标

一次页面加载可以拆成“服务端响应 → 首次可见 → 主体内容可见 → 可交互”几个阶段。

指标含义适用场景
TTFB(Time to First Byte)从导航开始到收到 HTML 第一个字节定位重定向、网络、CDN、服务端处理问题
FP(First Paint)浏览器第一次绘制像素,可能只是背景色判断白屏何时结束
FCP(First Contentful Paint)第一个文本、图片、SVG 等内容完成绘制判断用户何时首次看到有效内容
LCP(Largest Contentful Paint)视口内最大内容元素完成绘制衡量首屏主体内容出现速度
Speed Index页面可见区域在加载过程中填充的速度Lighthouse 等实验室环境中的视觉加载对比
TBT(Total Blocking Time)FCP 之后,长任务超出 50ms 部分的累计阻塞时间实验室中判断 JavaScript 是否阻塞主线程
TTI(Time to Interactive)页面达到稳定可交互状态的时间传统实验室指标,现在通常结合 TBT、INP 分析
DOMContentLoadedHTML 解析完成,延迟脚本执行完毕分析 DOM 构建阶段,不代表首屏渲染完成
Load页面及其依赖资源加载完成分析完整资源加载,不代表用户感知的首屏完成
自定义首屏时间业务定义的首屏关键内容全部展示完成异步数据、骨架屏、复杂活动页等业务场景

指标之间的关系

一般情况下可以粗略理解为:

导航开始
  → TTFB
  → FP
  → FCP
  → LCP / 业务首屏完成
  → 页面稳定可交互

这些事件并不保证严格按上述顺序发生。例如,异步数据可能让业务首屏完成时间晚于 load;用户也可能在 LCP 发生前就开始交互。

FCP 不等于首屏时间

  • FCP 只表示第一个有效内容出现,可能只是标题或骨架屏。
  • LCP 更接近“首屏主体出现”,但最大的元素不一定就是业务上的关键内容。
  • 首屏时间 没有统一的浏览器原生定义,应根据业务确定关键模块,并在它们真正渲染完成时埋点。

因此,监控首屏体验时通常组合使用 TTFB + FCP + LCP + 自定义首屏时间,而不是只看某一个指标。

3. 测量方式

3.1 Performance API

PerformanceNavigationTiming 可以拆分导航过程中的 DNS、TCP、TLS、请求响应和 DOM 处理耗时。

const navigation = performance.getEntriesByType('navigation')[0];

console.log('重定向耗时:', navigation.redirectEnd - navigation.redirectStart);
console.log('DNS 耗时:', navigation.domainLookupEnd - navigation.domainLookupStart);
console.log('TCP 耗时:', navigation.connectEnd - navigation.connectStart);
console.log(
  'TLS 耗时:',
  navigation.secureConnectionStart > 0
    ? navigation.connectEnd - navigation.secureConnectionStart
    : 0,
);
console.log('TTFB:', navigation.responseStart - navigation.startTime);
console.log('HTML 下载耗时:', navigation.responseEnd - navigation.responseStart);
console.log('DOMContentLoaded:', navigation.domContentLoadedEventEnd);
console.log('Load:', navigation.loadEventEnd);

需要注意:

  • responseStart - startTime 是从本次导航开始计算的 TTFB,包含重定向和连接建立等阶段。
  • responseStart - requestStart 只表示请求发出后等待首字节的时间,不是完整 TTFB。
  • 使用了连接复用或缓存时,DNS、TCP、TLS 耗时可能为 0

3.2 Paint Timing

通过 paint 类型可以获取 FP 和 FCP:

const paintEntries = performance.getEntriesByType('paint');

for (const entry of paintEntries) {
  console.log(entry.name, entry.startTime);
  // first-paint
  // first-contentful-paint
}

3.3 PerformanceObserver

PerformanceObserver 可以持续监听浏览器产生的性能条目。LCP 在页面加载过程中可能出现多个候选值,应保留最后一个有效候选值,并在页面隐藏时上报。

let latestLCP;

const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  latestLCP = entries[entries.length - 1];
});

observer.observe({
  type: 'largest-contentful-paint',
  buffered: true,
});

document.addEventListener(
  'visibilitychange',
  () => {
    if (document.visibilityState === 'hidden' && latestLCP) {
      console.log('LCP:', latestLCP.startTime, latestLCP);
      observer.disconnect();
    }
  },
  { once: true },
);

生产环境还要处理后台页面、往返缓存(bfcache)、页面提前关闭等边界情况,因此更推荐使用成熟的 web-vitals 库采集 Core Web Vitals。

3.4 web-vitals 库

import { onCLS, onINP, onLCP } from 'web-vitals';

function report(metric) {
  const body = JSON.stringify(metric);

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/performance', body);
    return;
  }

  fetch('/performance', {
    method: 'POST',
    body,
    keepalive: true,
  });
}

onLCP(report);
onINP(report);
onCLS(report);

3.5 自定义首屏时间

对于需要等待接口数据和图片渲染的页面,可以在首屏关键内容真正完成后打点:

async function renderFirstScreen() {
  const data = await fetchFirstScreenData();

  render(data);
  await waitForCriticalImages();

  // 等待浏览器至少完成一次绘制,再记录首屏完成时间
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      performance.mark('first-screen-complete');
      performance.measure('first-screen-duration', {
        start: 0,
        end: 'first-screen-complete',
      });

      const measure = performance
        .getEntriesByName('first-screen-duration')
        .at(-1);

      console.log('业务首屏时间:', measure.duration);
    });
  });
}

自定义指标必须先明确:

  • 哪些模块属于首屏关键内容;
  • 骨架屏算“内容出现”还是只算占位;
  • 图片加载失败时何时结束等待;
  • 客户端路由切换是从点击、路由开始,还是数据请求开始计时。

4. 实验室数据与真实用户数据

  • 实验室数据(Lab):在固定设备和网络条件下重复测试,例如 Lighthouse。适合开发阶段定位问题、对比优化前后结果。
  • 真实用户数据(Field / RUM):采集真实用户设备、网络和交互产生的数据。适合判断线上用户的实际体验。

两者应结合使用:实验室数据负责复现和诊断,真实用户数据负责发现真实分布与长尾问题。线上统计建议按页面、设备、网络、地区等维度拆分,并关注 P75,而不只看平均值。

5. 常见面试题

Q:performance.now()Date.now() 有什么区别?

  • Date.now() 返回 Unix 时间戳,单位为毫秒,可能受系统时间调整影响。
  • performance.now() 返回相对于 performance.timeOrigin 的高精度时间,单调递增,不受系统时钟校准影响,更适合测量耗时。

performance.now() 的单位仍然是毫秒,只是可以包含小数;出于安全和隐私考虑,浏览器可能降低其实际精度。

Q:白屏时间、FCP 和首屏时间有什么区别?

  • 白屏时间:通常用 FP 或 FCP 近似,表示页面何时开始出现像素或有效内容。
  • FCP:第一个有效内容完成绘制。
  • 首屏时间:首屏关键内容完成展示,通常使用 LCP 或业务自定义指标衡量。

Q:如何优化 LCP?

先根据 LCP 的耗时组成定位瓶颈,再选择优化方式:

  1. 降低 TTFB:使用 CDN、页面缓存、SSR 流式输出,优化服务端查询和渲染。
  2. 尽早发现 LCP 资源:让关键图片出现在初始 HTML 中,必要时使用 preload,避免通过 JavaScript 延迟注入。
  3. 提高资源优先级:关键图片可以使用 fetchpriority="high",不要对首屏 LCP 图片使用懒加载。
  4. 减小资源体积:使用合适尺寸和格式的图片,开启压缩与缓存。
  5. 减少渲染延迟:内联少量关键 CSS,拆分长任务,减少阻塞渲染的 CSS 和同步 JavaScript。

Q:TBT 和 INP 有什么区别?

  • TBT 统计加载阶段主线程长任务造成的阻塞,主要用于实验室测试,即使用户没有交互也能测量。
  • INP 来自实际交互,衡量事件等待、事件处理和下一帧绘制的整体延迟,更适合表示真实用户的交互体验。

Q:为什么不能只看 DOMContentLoadedload

它们描述的是浏览器加载生命周期,而不是用户看到的内容:

  • SPA 可能在 DOMContentLoaded 后才请求并渲染首屏数据;
  • load 可能等待首屏之外的图片,也可能早于异步业务内容完成;
  • 页面即使已经触发 load,仍可能因为长任务而无法及时响应用户。

所以应优先使用以用户体验为中心的 FCP、LCP、INP、CLS,以及与业务匹配的自定义指标。