首页/文章/前端性能

前端常见的性能指标采集

2025-07-27
437615 分钟
...

前言

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

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

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

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

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

一、定义

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

二、计算方式

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

三、采集方法

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

现代浏览器提供了 PerformanceObserver API 来监听 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,   
    });
  }
});


observer.observe({ type: "paint", buffered: true });

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

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

import { onFCP } from 'web-vitals';


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,但可以通过以下方式近似实现:

(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

          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)
      })()

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

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

一、定义

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

二、计算方式

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

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

三、采集方法

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

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


const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1]; 
  
  console.log("LCP 数据:", {
    renderTime: lastEntry.renderTime, 
    loadTime: lastEntry.loadTime,     
    startTime: lastEntry.startTime,   
    element: lastEntry.element        
  });
});


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

(二)使用 Web Vitals 库

import { onLCP } from 'web-vitals'


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 来监听布局偏移事件并获取相关数据。以下是示例代码:


      const observer = new PerformanceObserver((entryList) => {
        const entries = entryList.getEntries();
        let totalCLS = 0;

        entries.forEach((entry) => {
          
          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 库

import { onCLS } from 'web-vitals'


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

参考上述示意图图中的 First Consistently Interactive 即为 TTI )。

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

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

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

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

如果 23 步骤得到的 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)


function getTTFB() {
    const [pageNav] = performance.getEntriesByType('navigation');
    if (!pageNav) return null;
    
    
    
    const ttfb = pageNav.responseStart - pageNav.fetchStart;
    
    return {
        value: ttfb,                  
        timestamp: new Date(pageNav.startTime).toISOString(),
        isSupport: true
    };
}


const ttfbResult = getTTFB();
console.log("页面TTFB:", ttfbResult);

(二)使用 Web Vitals 库

import { onTTFB } from 'web-vitals';

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-timing 和 paint-timing 事件来计算:

<!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 = []; 
        }
        recordInteraction(inputDelay, processingTime, paintDelay) {
          const totalResponseTime = inputDelay + processingTime + paintDelay;
          this.interactionRecords.push(totalResponseTime);

          
          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;

            
            const sortedWindow = [...window].sort((a, b) => a - b);
            const trimmedWindow = sortedWindow.slice(0, -1); 

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

          
          let remainingResult = 0;
          if (this.interactionRecords.length > 0) {
            const sortedRemaining = [...this.interactionRecords].sort(
              (a, b) => a - b
            );
            
            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))];
        }
      }
      
      const inpCalculator = new INPCalculator();

      function simulateUserInteraction() {
        
        const inputDelay = Math.random() * 100;

        
        const processingTime = Math.random() * 200;

        
        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`
        );
      }
      
      document.querySelectorAll(".item").forEach((item) => {
        item.addEventListener("click", simulateUserInteraction);
      });
      
      setInterval(() => {
        const inpValue = inpCalculator.calculate();
        if (inpValue !== null) {
          console.log(`[INP计算结果] 当前INP值: ${inpValue.toFixed(2)}ms`);
        }
      }, 5000); 

      
      window.addEventListener("beforeunload", () => {
        const finalINP = inpCalculator.calculate();
        if (finalINP !== null) {
          console.log(`[最终INP结果] ${finalINP.toFixed(2)}ms`);
          
        }
      });
    </script>
  </body>
</html>

(二)使用 Web Vitals 库

import { onINP } from 'web-vitals';

onINP(console.log);

结尾

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

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
前端性能2025-08-07
JS篇之性能优化
很多时候面试官,面试你的时候会问你:同学你在项目中做过哪些性能优化呢? 可能你是这样回答的: 1. 无用代码的删除,减小代码打的体积 2. 使用cdn加速 3. 图片压缩 4. 分包 5. 骨架屏 ........ 其实性能优化是个很宽泛的东西,可以从多方面进行入手 性能优化不是视觉体验打分,网上部分评判如下 | 核心指标 | 定义 | 优化方向 | | | | | | LCP(最大内容绘制) | 首屏最大内容加载完成时间 | 图片压缩...
学习面试
nuxt2025-10-22
Nuxt SSR 与 Next.js SSR
🧭 引言 在现代前端生态中, Nuxt(Vue) 与 Next.js(React) 是最成熟的两大全栈框架。 它们都具备 SSR(Server Side Rendering) 能力,但实现原理与运行机制差异巨大。 如果你在使用 Nuxt 构建 SSR 项目,却发现“路由跳转后不再 SSR”, 而在 Next.js 中同样操作却会重新发起服务端渲染——这不是 bug,而是 理念差异 。 本文将详细对比这两种框架的 SSR 行为,帮助你彻...
学习面试
vue2025-10-22
shallowRef 与 ref 的区别、场景与坑位
结论先行: 会 深度 地把你放进去的对象转成响应式(递归代理),因此"对象内部的改动"也会触发视图更新。 只在 顶层 做依赖追踪: 更换 才触发更新 ;若你只是"在原对象上改属性",不会触发,除非手动 。 1\. 快速对比 维度 追踪粒度 深度(递归) 仅顶层 对象内部属性变更 会触发更新 不会 触发(除非 或重新赋值) 性能 有递归 & 依赖跟踪开销 更轻量,适合大对象/第三方实例 适用对象 普通标量、普通对象、数组 第三方实例(图表...
学习面试
vue2025-10-20
Vue 3 中的 setup执行时机与异步逻辑详解
Vue 3 中的 执行时机与异步逻辑详解 本文详细介绍 Vue 3 组合式 API 的核心函数 :它在组件生命周期中的执行时机、异步行为( 的影响)、常见用法和最佳实践。 一、 的执行时机 是 Vue 3 组件实例创建后、渲染前执行的函数。 它相当于 Vue 2 中的 和 的合体。 执行流程: 也就是说: 所有响应式数据( 、 、 )都在这里定义; 所有生命周期钩子( 、 等)都在这里注册; 模板渲染会等待 执行完成(或异步返回的 Pr...
学习面试
八股文2025-10-13
cookie跨域介绍
Cookie 跨域问题详解 在 Web 开发中, Cookie 是最常见的客户端存储机制之一,用于记录用户登录状态、偏好设置、会话信息等。然而,当涉及到 跨域请求 时,Cookie 的行为往往变得复杂,尤其在现代浏览器的安全策略下,跨域 Cookie 的传递和写入都有严格的限制。 本文将从基础概念开始,逐步讲解 Cookie 的跨域机制、SameSite 属性、CORS 配置及常见问题解决方案。 一、Cookie 的基础概念 Cooki...
学习面试
八股文2025-10-09
JavaScript 闭包详解
JavaScript 闭包(Closure)详解 一、什么是闭包 闭包(Closure) 是 JavaScript 中一个非常核心且常被问到的概念。简单来说: 闭包是一个函数,它可以“记住”并访问其定义时所在的词法作用域,即使这个函数在其作用域之外被调用。 换句话说,当一个函数“嵌套”在另一个函数中,并且 内部函数引用了外部函数的变量 时,就形成了闭包。 示例: 虽然 已经执行完毕,但 依然可以访问 中的 。这就是闭包。 二、闭包的形成...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录