前端常见的性能指标采集

前言

作为前端,我们的任务便是给用户一个好的产品体验感,不管是从首次进入页面的加载时长,还是对于交互时页面的响应流畅度,都是我们应该关注的点。

RAIL模型是由谷歌提出的,一种以用户为中心的性能模型,RAIL分别代表Web应用生命周期的四个方面:响应、动画、空闲、加载。

一般不管是页面打开还是流程交互,又或者是网络反馈,这也操作我们应该尽量在1000ms内完成,用户的体验感才不会差,用户留存率也将得到提升。

下面看一下我们常见的一些性能指标与采集方法。

FCP(First Contentful Paint,首次内容绘制)

一、定义

FCP 是指浏览器首次将页面中的​​任何内容​​(如文本、图像、SVG 等)绘制到屏幕上的时间点。它是衡量页面加载初期用户体验的一个关键指标,反映了用户能够看到页面有实际内容的时间。

二、计算方式

浏览器在解析 HTML、CSS 和 JavaScript 等资源的过程中,一旦有可见的内容被绘制到屏幕上,就会记录 FCP 时间。例如,当页面中的标题文本、首张图片等元素开始显示时,对应的时刻即为 FCP 时间。

三、采集方法

(一)使用 Performance API(浏览器原生 API)

现代浏览器提供了 PerformanceObserver API 来监听 FCP 事件并获取相关数据。以下是示例代码:

javascript 复制代码
// 创建 PerformanceObserver 实例,监听 FCP 事件
const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const fcpEntry = entries.find(entry => entry.name === 'first-contentful-paint');
  if (fcpEntry) {
    console.log("FCP 数据:", {
      renderTime: fcpEntry.renderTime, // 渲染时间(如果可用)
      loadTime: fcpEntry.loadTime,     // 加载时间(如果可用)
      startTime: fcpEntry.startTime,   // FCP 发生的时间(相对于页面加载开始的时间)
    });
  }
});

// 开始监听 FCP 事件
observer.observe({ type: "paint", buffered: true });

(二)使用 Web Vitals 库(Google 推荐)

Google 提供了 web-vitals 库,可方便地采集 FCP 等核心 Web 指标(CWV)。

javascript 复制代码
import { onFCP } from 'web-vitals';

// 采集 FCP 数据
onFCP(console.log);

FMP(First Meaningful Paint,首次有效绘制)

一、定义

FMP(First Meaningful Paint)即首次有效绘制,是指浏览器首次绘制出对用户​​有实际意义的内容​ ​的时间点。与FCP(首次内容绘制)不同,FCP只是页面开始绘制内容的时间,而FMP关注的是页面中​​对用户有实际价值的内容​​开始呈现的时刻。

二、与FCP的区别

指标 定义 关注点
FCP 首次绘制任何内容的时间 页面开始呈现内容的时间点
FMP 首次绘制有意义内容的时间 用户感知到页面主要内容的时间点

FMP通常比FCP更能反映用户对页面加载速度的真实感受,因为它关注的是页面核心内容的呈现,而不是简单的DOM元素绘制。

三、计算方式

FMP的计算比FCP更为复杂,因为需要判断哪些内容对用户是有"意义"的。浏览器通常会通过以下方式估算:

  1. 分析页面中不同元素的视觉重要性
  2. 评估元素在页面布局中的位置和大小
  3. 结合机器学习模型判断哪些内容对用户最有价值

四、采集方法

1. 原生方法(实验性)

目前浏览器没有直接提供FMP的PerformanceObserver API,但可以通过以下方式近似实现:

ini 复制代码
(function () {
        // 配置参数
        const THRESHOLD = 0; // 变化量的阈值,可根据页面调整
        let mutationCount = 0;
        let fmpDetected = false;
        let fmpTime = null;

        // 判断是否是"有意义"的节点(如文本节点、图片等)
        function isMeaningfulNode(node) {
          if (node.nodeType === Node.TEXT_NODE) {
            return node.textContent.trim().length > 0; // 非空文本节点
          }
          if (node.nodeType === Node.ELEMENT_NODE) {
            // 可以扩展这里,比如判断是否是图片、视频、div 等有内容的元素
            return true; // 简化:所有元素节点都算
          }
          return false;
        }

        // 计算本次变化的影响值
        function calculateMutationImpact(mutations) {
          let count = 0;
          mutations.forEach((mutation) => {
            if (mutation.type === "childList") {
              mutation.addedNodes.forEach((node) => {
                if (isMeaningfulNode(node)) {
                  count += 1;
                }
                // 如果是元素节点,递归检查其子节点中的文本节点
                if (node.nodeType === Node.ELEMENT_NODE) {
                  const textNodes = node.querySelectorAll?.("body *")?.length
                    ? Array.from(node.querySelectorAll("body *")).filter(
                        isMeaningfulNode
                      ).length
                    : 0;
                  // 注意:querySelectorAll 在新增的节点上可能不生效,所以更安全的方式是遍历子树
                  // 这里简化处理,仅统计直接子节点
                  let childTextNodes = 0;
                  const traverse = (el) => {
                    el.childNodes.forEach((child) => {
                      if (
                        child.nodeType === Node.TEXT_NODE &&
                        child.textContent.trim().length > 0
                      ) {
                        childTextNodes += 1;
                      } else if (child.nodeType === Node.ELEMENT_NODE) {
                        traverse(child);
                      }
                    });
                  };
                  traverse(node);
                  count += childTextNodes;
                }
              });
            }
          });
          return count;
        }

        // 开始监听
        const observer = new MutationObserver((mutations) => {
          if (fmpDetected) return; // 已经检测到 FMP,不再处理

          const impact = calculateMutationImpact(mutations);
          mutationCount += impact;

          if (mutationCount >= THRESHOLD) {
            fmpDetected = true;
            fmpTime = performance.now(); // 记录当前时间戳
            console.log("FMP detected at:", fmpTime, "ms");
            observer.disconnect(); // 停止观察
          }
        });

        // 开始观察整个文档,包括子树和属性变化(可根据需要调整)
        observer.observe(document.documentElement, {
          childList: true, // 监听子节点变化
          subtree: true, // 监听所有后代节点
          attributes: false, // 不监听属性变化(可按需开启)
          characterData: false, // 不监听文本变化(已通过 childList 捕获)
        });

        // 设置超时保护,避免页面一直不触发 FMP
        setTimeout(() => {
          if (!fmpDetected) {
            console.warn("FMP detection timeout, using fallback timestamp.");
            fmpTime = performance.now();
            observer.disconnect();
          }
        }, 1000); // 1 秒超时
      })();

ps : 方法引用于火山引擎FMP检查方法

LCP(Largest Contentful Paint,最大内容绘制)

一、定义

LCP(Largest Contentful Paint)是指页面从开始加载到最大可见内容元素绘制完成的时间点。最大可见内容元素通常是指页面上尺寸最大的文本块、图像、视频等元素,它代表了页面的核心内容呈现给用户的时间。LCP 是衡量页面加载性能的关键指标之一,能够反映用户感知到页面主要内容加载完成的时间。

二、计算方式

LCP 计算的是页面中最大可见内容元素的渲染完成时间。浏览器会持续监测页面上可见元素的大小和绘制时间,找出最大的那个可见元素并记录其绘制完成的时间作为 LCP 时间。最大内容元素通常包括:

  • 图片(<img>
  • 视频(<video>
  • 大型 <div><p> 文本块
  • 其他占据较大可视区域的元素

三、采集方法

(一)使用 Performance API(浏览器原生 API)

现代浏览器提供了 PerformanceObserver API 来监听 LCP 事件并获取相关数据。以下是示例代码:

javascript 复制代码
// 创建 PerformanceObserver 实例,监听 LCP 事件
const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1]; // LCP 可能有多个记录,取最后一个(最新的)
  
  console.log("LCP 数据:", {
    renderTime: lastEntry.renderTime, // 渲染时间(如果可用)
    loadTime: lastEntry.loadTime,     // 加载时间(如果可用)
    startTime: lastEntry.startTime,   // LCP 发生的时间(相对于页面加载开始的时间)
    element: lastEntry.element        // 导致 LCP 的 DOM 元素
  });
});

// 开始监听 LCP 事件
observer.observe({ type: "largest-contentful-paint", buffered: true });

(二)使用 Web Vitals 库

javascript 复制代码
import { onLCP } from 'web-vitals'

// 采集 LCP 数据
onLCP(console.log);

CLS(Cumulative Layout Shift,累积布局偏移)

一、定义

CLS(Cumulative Layout Shift)即累积布局偏移,是衡量页面在加载和交互过程中元素意外移动程度的指标。它量化了页面内容在视觉上的稳定性,具体指从页面开始加载到其生命周期结束(如用户离开页面)期间,所有意外布局偏移的累积值。布局偏移是指页面上的元素在没有任何用户交互的情况下位置发生变化的现象,比如图片加载后导致下方文本突然上移、广告插入导致内容区域抖动等情况。

二、计算方式

CLS 的计算基于以下两个关键因素:

  1. ​影响分数(Impact Fraction)​​:衡量受布局偏移影响的视口面积比例。计算公式为:

    • 影响分数 = 移动前元素占据的视口面积 + 移动后元素占据的视口面积 / 2
  2. ​距离分数(Distance Fraction)​​:衡量元素移动的距离与视口高度或宽度的最大比例。计算公式为:

    • 距离分数 = 元素移动的垂直或水平距离 / 视口高度或宽度的最大值

最终的 CLS 值计算公式为:

  • CLS = 影响分数 × 距离分数

每个意外的布局偏移都会产生一个分数,页面的 CLS 是所有意外布局偏移分数的总和。需要注意的是,只有那些​​没有用户交互​​(如点击、滚动等)触发的布局变化才会被计入 CLS。

三、采集方法

(一)使用 Performance API(浏览器原生 API)

现代浏览器提供了 PerformanceObserver API 来监听布局偏移事件并获取相关数据。以下是示例代码:

javascript 复制代码
// 创建 PerformanceObserver 实例,监听布局偏移事件
      const observer = new PerformanceObserver((entryList) => {
        const entries = entryList.getEntries();
        let totalCLS = 0;

        entries.forEach((entry) => {
          // 只关注意外的布局偏移(hadRecentInput 为 false)
          if (!entry.hadRecentInput) {
            totalCLS += entry.value;
            console.log("布局偏移事件:", {
              value: entry.value, // 此次偏移的分数
              time: entry.startTime, // 偏移发生的时间
              element: entry.sources[0]?.node?.tagName || "未知元素", // 导致偏移的元素
            });
          }
        });

        console.log("累计 CLS 值:", totalCLS);
      });

      // 开始监听布局偏移事件
      observer.observe({ type: "layout-shift", buffered: true });

(二)使用 Web Vitals 库

javascript 复制代码
import { onCLS } from 'web-vitals'

// 采集 LCP 数据
onCLS(console.log);

TTI(Time to Interactive,可交互时间)

一、定义

TTI(Time to Interactive)即​​可交互时间​​,是指页面从开始加载到主要子资源都已加载完成,并且主线程空闲足够长的时间(通常为5秒),能够可靠地对用户交互做出及时响应的时间点。它是衡量页面交互性能的关键指标,反映了用户何时可以与页面进行流畅的交互操作。

TTI关注的是页面不仅完成了加载,而且达到了可以稳定响应用户输入的状态,是用户体验的重要指标之一。

二、计算方式

TTI的计算比其他性能指标更为复杂,因为它需要评估页面的​​交互响应能力​​而不仅仅是资源加载状态。具体计算方式如下:

  1. ​主线程空闲检测​:浏览器监测主线程在5秒内是否没有长时间任务(超过50毫秒的任务)运行
  2. ​网络空闲检测​:页面的主要子资源(如JavaScript、CSS等)已经加载完成
  3. ​事件响应能力​:页面能够可靠地响应用户交互事件(如点击、滚动等)

TTI的计算通常基于以下条件同时满足:

  • 页面的主要内容已经渲染完成
  • 没有长时间运行的JavaScript任务阻塞主线程
  • 页面可以快速响应用户输入

三、采集方法

TTI并没有提供准确的API

arduino 复制代码
参考上述示意图(图中的 First Consistently Interactive 即为 TTI )。

从起始点(一般选择 FCP 或 FMP)时间开始,向前搜索一个不小于 5s 的静默窗口期。

静默窗口期:窗口所对应的时间内没有 Long Task,且进行中的网络请求数不超过 2 个。

找到静默窗口期后,从静默窗口期向后搜索到最近的一个 Long Task,Long Task 的结束时间即为 TTI。

如果没有找到 Long Task,以起始点时间作为 TTI。

如果 2、3 步骤得到的 TTI < DOMContentLoadedEventEnd,以 DOMContentLoadedEventEnd 作为TTI。

TTFB(Time to First Byte,首字节时间)

一、定义

TTFB(Time to First Byte)是指​​浏览器从发起请求到接收到服务器响应的第一个字节所花费的时间​​。这个指标衡量的是服务器响应速度和网络延迟的综合表现,是评估服务器性能和网络状况的关键指标。

TTFB包括三个主要阶段:

  1. ​DNS解析时间​:将域名解析为IP地址所需的时间
  2. ​TCP连接建立时间​:与服务器建立TCP连接的时间
  3. ​请求发送和第一个字节接收时间​:发送HTTP请求到接收到第一个响应字节的时间

二、计算方式

TTFB的计算公式为:TTFB = 响应第一个字节的时间 - 请求发起的时间

在浏览器中,可以通过以下方式获取TTFB:

  1. ​Navigation Timing API​ :通过performance.timing对象获取
  2. ​Resource Timing API​ :通过performance.getEntriesByType('resource')获取特定资源的TTFB

三、采集方法

(一)使用Navigation Timing API(测量整个页面的TTFB)

javascript 复制代码
// 使用Navigation Timing API测量页面加载的TTFB
function getTTFB() {
    const [pageNav] = performance.getEntriesByType('navigation');
    if (!pageNav) return null;
    
    // TTFB = responseStart - requestStart
    // 但更准确的定义是:timeToFirstByte = responseStart - fetchStart
    const ttfb = pageNav.responseStart - pageNav.fetchStart;
    
    return {
        value: ttfb,                  // TTFB时间(毫秒)
        timestamp: new Date(pageNav.startTime).toISOString(),
        isSupport: true
    };
}

// 获取并输出TTFB
const ttfbResult = getTTFB();
console.log("页面TTFB:", ttfbResult);

(二)使用 Web Vitals 库

javascript 复制代码
import { onTTFB } from 'web-vitals';
// 采集 TTFB 数据
onTTFB(console.log);

INP(Interaction to Next Paint,交互到下一绘制)

一、定义

​INP(Interaction to Next Paint)​ ​ 是 Google 提出的新一代 ​​核心网页指标(Core Web Vitals)​ ​,用于衡量 ​​用户与页面交互后,页面响应交互并完成视觉更新所需的时间​ ​。它取代了原先的 ​​FID(First Input Delay,首次输入延迟)​​,成为更全面的交互性能评估指标。

INP 的核心关注点是:

​从用户发起交互(如点击、输入、滚动等)到页面完成响应并更新视觉反馈(下一帧绘制)的总时间​​。


二、计算方式

INP 的计算基于 ​​所有用户交互事件的响应延迟​ ​,取其中 ​​最差交互(最长延迟)​​ 作为最终值,并允许一定的容错范围(通过"交互窗口"机制平滑异常值)。具体逻辑如下:

  1. ​记录所有交互事件​ ​:

    包括点击、输入、滚动、触摸等用户主动触发的操作。

  2. ​计算每个交互的响应延迟​​:

    • ​交互延迟(Interaction Delay)​:从用户发起交互到浏览器能够开始处理该交互的时间(类似 FID 的"输入延迟"部分)。
    • ​交互处理耗时(Processing Time)​:浏览器处理交互逻辑(如 JavaScript 执行、样式计算、布局更新等)的时间。
    • ​绘制延迟(Paint Delay)​:从交互处理完成到下一帧绘制完成的时间(类似 FCP 的"绘制时间"部分)。

    ​单个交互的响应时间 = 交互延迟 + 处理耗时 + 绘制延迟​​。

  3. ​选取最差交互​ ​:

    统计页面生命周期内所有交互的响应时间,取 ​​最长的 90% 分位数​​(即排除极端异常值后的最大延迟)作为 INP 值。

  4. ​容错机制(交互窗口)​​:

    • 如果用户在某个交互后 ​短时间内(默认 50 毫秒)发起新的交互​,则这两个交互会被合并为一个"交互会话",避免因快速连续操作导致指标失真。
    • 这种机制确保 INP 更真实地反映用户实际体验,而非单次操作的极端情况。

三、采集方法

(一)使用 PerformanceObserver API(现代浏览器)

INP 是较新的指标(Chrome 117+ 原生支持),可通过 PerformanceObserver 监听 event-timingpaint-timing 事件来计算:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>INP检测实现(原理版)</title>
    <style>
      .container {
        margin: 20px;
      }
      .item {
        padding: 10px;
        margin: 5px;
        background: #eee;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="item">点击我 (1)</div>
      <div class="item">点击我 (2)</div>
      <div class="item">点击我 (3)</div>
      <div class="item">点击我 (4)</div>
    </div>

    <script>
      class INPCalculator {
        constructor() {
          this.interactionRecords = []; // 存储所有交互记录
          this.interactionWindows = []; // 存储交互窗口(每50次互动为一组)
        }
        recordInteraction(inputDelay, processingTime, paintDelay) {
          const totalResponseTime = inputDelay + processingTime + paintDelay;
          this.interactionRecords.push(totalResponseTime);

          // 每50次互动为一组
          if (this.interactionRecords.length % 50 === 0) {
            this.interactionWindows.push([...this.interactionRecords]);
            this.interactionRecords = []; // 重置当前窗口记录
          }
        }
        calculate() {
          if (
            this.interactionRecords.length === 0 &&
            this.interactionWindows.length === 0
          ) {
            console.warn("未检测到任何交互记录");
            return null;
          }

          // 处理完整窗口组
          const windowResults = this.interactionWindows.map((window) => {
            if (window.length === 0) return 0;

            // 忽略最高1次延迟(每50次互动)
            const sortedWindow = [...window].sort((a, b) => a - b);
            const trimmedWindow = sortedWindow.slice(0, -1); // 移除最大值

            return this.calculatePercentile(trimmedWindow, 75);
          });

          // 处理剩余未满50次的互动
          let remainingResult = 0;
          if (this.interactionRecords.length > 0) {
            const sortedRemaining = [...this.interactionRecords].sort(
              (a, b) => a - b
            );
            // 按比例忽略最高记录(如49次互动忽略1次)
            const ignoreCount = Math.ceil(sortedRemaining.length / 50);
            const trimmedRemaining = sortedRemaining.slice(0, -ignoreCount);
            remainingResult = this.calculatePercentile(trimmedRemaining, 75);
          }

          // 合并所有窗口结果
          const allResults = [...windowResults];
          if (remainingResult > 0) allResults.push(remainingResult);

          return allResults.length > 0
            ? this.calculatePercentile(allResults, 75)
            : null;
        }

        calculatePercentile(data, percentile) {
          if (data.length === 0) return 0;
          const index = Math.ceil((data.length * percentile) / 100) - 1;
          return data[Math.max(0, Math.min(index, data.length - 1))];
        }
      }
      // 2. 模拟交互数据生成
      const inpCalculator = new INPCalculator();

      function simulateUserInteraction() {
        // 模拟输入延迟(0-100ms)
        const inputDelay = Math.random() * 100;

        // 模拟处理耗时(0-200ms)
        const processingTime = Math.random() * 200;

        // 模拟绘制延迟(0-50ms)
        const paintDelay = Math.random() * 50;

        // 记录交互
        inpCalculator.recordInteraction(inputDelay, processingTime, paintDelay);

        console.log(
          `[模拟] 交互延迟: ${inputDelay.toFixed(2)}ms, ` +
            `处理耗时: ${processingTime.toFixed(2)}ms, ` +
            `绘制延迟: ${paintDelay.toFixed(2)}ms, ` +
            `总响应时间: ${(inputDelay + processingTime + paintDelay).toFixed(
              2
            )}ms`
        );
      }
      // 3. 绑定交互事件
      document.querySelectorAll(".item").forEach((item) => {
        item.addEventListener("click", simulateUserInteraction);
      });
      // 4. 定期计算并输出INP
      setInterval(() => {
        const inpValue = inpCalculator.calculate();
        if (inpValue !== null) {
          console.log(`[INP计算结果] 当前INP值: ${inpValue.toFixed(2)}ms`);
        }
      }, 5000); // 每5秒计算一次上报

      // 5. 页面卸载前最终计算
      window.addEventListener("beforeunload", () => {
        const finalINP = inpCalculator.calculate();
        if (finalINP !== null) {
          console.log(`[最终INP结果] ${finalINP.toFixed(2)}ms`);
          // 实际应用中可在此处上报数据
        }
      });
    </script>
  </body>
</html>

(二)使用 Web Vitals 库

javascript 复制代码
import { onINP } from 'web-vitals';
// 采集 INP 数据
onINP(console.log);

结尾

这是我们一些比较常见的前端性能面板里面比较常见的一些性能指标,这里,特殊业务可能有定制化需求,也可视情况而定。

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
yunteng5214 小时前
通用架构(同城双活)(单点接入)
架构·同城双活·单点接入