前言

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

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

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

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

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

一、定义

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

二、计算方式

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

三、采集方法

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

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

 1
 2const observer = new PerformanceObserver((entryList) => {
 3  const entries = entryList.getEntries();
 4  const fcpEntry = entries.find(entry => entry.name === 'first-contentful-paint');
 5  if (fcpEntry) {
 6    console.log("FCP 数据:", {
 7      renderTime: fcpEntry.renderTime, 
 8      loadTime: fcpEntry.loadTime,     
 9      startTime: fcpEntry.startTime,   
10    });
11  }
12});
13
14
15observer.observe({ type: "paint", buffered: true });

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

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

 1import { onFCP } from 'web-vitals';
 2
 3
 4onFCP(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,但可以通过以下方式近似实现:

 1(function () {
 2        // 配置参数
 3        const THRESHOLD = 0
 4        let mutationCount = 0
 5        let fmpDetected = false
 6        let fmpTime = null
 7
 8        // 判断是否是“有意义”的节点(如文本节点、图片等)
 9        function isMeaningfulNode(node) {
10          if (node.nodeType === Node.TEXT_NODE) {
11            return node.textContent.trim().length > 0
12          }
13          if (node.nodeType === Node.ELEMENT_NODE) {
14            // 可以扩展这里,比如判断是否是图片、视频、div 等有内容的元素
15            return true
16          }
17          return false
18        }
19
20        // 计算本次变化的影响值
21        function calculateMutationImpact(mutations) {
22          let count = 0
23          mutations.forEach((mutation) => {
24            if (mutation.type === "childList") {
25              mutation.addedNodes.forEach((node) => {
26                if (isMeaningfulNode(node)) {
27                  count += 1
28                }
29                // 如果是元素节点,递归检查其子节点中的文本节点
30                if (node.nodeType === Node.ELEMENT_NODE) {
31                  const textNodes = node.querySelectorAll?.("body *")?.length
32                    ? Array.from(node.querySelectorAll("body *")).filter(
33                        isMeaningfulNode
34                      ).length
35                    : 0
36                  // 注意:querySelectorAll 在新增的节点上可能不生效,所以更安全的方式是遍历子树
37                  // 这里简化处理,仅统计直接子节点
38                  let childTextNodes = 0
39                  const traverse = (el) => {
40                    el.childNodes.forEach((child) => {
41                      if (
42                        child.nodeType === Node.TEXT_NODE &&
43                        child.textContent.trim().length > 0
44                      ) {
45                        childTextNodes += 1
46                      } else if (child.nodeType === Node.ELEMENT_NODE) {
47                        traverse(child)
48                      }
49                    })
50                  }
51                  traverse(node)
52                  count += childTextNodes
53                }
54              })
55            }
56          })
57          return count
58        }
59
60        // 开始监听
61        const observer = new MutationObserver((mutations) => {
62          if (fmpDetected) return
63
64          const impact = calculateMutationImpact(mutations)
65          mutationCount += impact
66
67          if (mutationCount >= THRESHOLD) {
68            fmpDetected = true
69            fmpTime = performance.now()
70            console.log("FMP detected at:", fmpTime, "ms")
71            observer.disconnect()
72          }
73        })
74
75        // 开始观察整个文档,包括子树和属性变化(可根据需要调整)
76        observer.observe(document.documentElement, {
77          childList: true, // 监听子节点变化
78          subtree: true, // 监听所有后代节点
79          attributes: false, // 不监听属性变化(可按需开启)
80          characterData: false, // 不监听文本变化(已通过 childList 捕获)
81        })
82
83        // 设置超时保护,避免页面一直不触发 FMP
84        setTimeout(() => {
85          if (!fmpDetected) {
86            console.warn("FMP detection timeout, using fallback timestamp.")
87            fmpTime = performance.now()
88            observer.disconnect()
89          }
90        }, 1000)
91      })()

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

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

一、定义

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

二、计算方式

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

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

三、采集方法

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

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

 1
 2const observer = new PerformanceObserver((entryList) => {
 3  const entries = entryList.getEntries();
 4  const lastEntry = entries[entries.length - 1]; 
 5  
 6  console.log("LCP 数据:", {
 7    renderTime: lastEntry.renderTime, 
 8    loadTime: lastEntry.loadTime,     
 9    startTime: lastEntry.startTime,   
10    element: lastEntry.element        
11  });
12});
13
14
15observer.observe({ type: "largest-contentful-paint", buffered: true });

(二)使用 Web Vitals 库

 1import { onLCP } from 'web-vitals'
 2
 3
 4onLCP(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 来监听布局偏移事件并获取相关数据。以下是示例代码:

 1
 2      const observer = new PerformanceObserver((entryList) => {
 3        const entries = entryList.getEntries();
 4        let totalCLS = 0;
 5
 6        entries.forEach((entry) => {
 7          
 8          if (!entry.hadRecentInput) {
 9            totalCLS += entry.value;
10            console.log("布局偏移事件:", {
11              value: entry.value, 
12              time: entry.startTime, 
13              element: entry.sources[0]?.node?.tagName || "未知元素", 
14            });
15          }
16        });
17
18        console.log("累计 CLS 值:", totalCLS);
19      });
20
21      
22      observer.observe({ type: "layout-shift", buffered: true });

(二)使用 Web Vitals 库

 1import { onCLS } from 'web-vitals'
 2
 3
 4onCLS(console.log);

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

一、定义

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

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

二、计算方式

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

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

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

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

三、采集方法

TTI并没有提供准确的API

 1参考上述示意图图中的 First Consistently Interactive 即为 TTI )。
 2
 3从起始点一般选择 FCP FMP时间开始向前搜索一个不小于 5s 的静默窗口期
 4
 5静默窗口期窗口所对应的时间内没有 Long Task且进行中的网络请求数不超过 2
 6
 7找到静默窗口期后从静默窗口期向后搜索到最近的一个 Long TaskLong Task 的结束时间即为 TTI
 8
 9如果没有找到 Long Task以起始点时间作为 TTI
10
11如果 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)

 1
 2function getTTFB() {
 3    const [pageNav] = performance.getEntriesByType('navigation');
 4    if (!pageNav) return null;
 5    
 6    
 7    
 8    const ttfb = pageNav.responseStart - pageNav.fetchStart;
 9    
10    return {
11        value: ttfb,                  
12        timestamp: new Date(pageNav.startTime).toISOString(),
13        isSupport: true
14    };
15}
16
17
18const ttfbResult = getTTFB();
19console.log("页面TTFB:", ttfbResult);

(二)使用 Web Vitals 库

 1import { onTTFB } from 'web-vitals';
 2
 3onTTFB(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 事件来计算:

 1<!DOCTYPE html>
 2<html lang="en">
 3  <head>
 4    <meta charset="UTF-8" />
 5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 6    <title>INP检测实现(原理版)</title>
 7    <style>
 8      .container {
 9        margin: 20px;
10      }
11      .item {
12        padding: 10px;
13        margin: 5px;
14        background: #eee;
15        cursor: pointer;
16      }
17    </style>
18  </head>
19  <body>
20    <div class="container">
21      <div class="item">点击我 (1)</div>
22      <div class="item">点击我 (2)</div>
23      <div class="item">点击我 (3)</div>
24      <div class="item">点击我 (4)</div>
25    </div>
26
27    <script>
28      class INPCalculator {
29        constructor() {
30          this.interactionRecords = []; 
31          this.interactionWindows = []; 
32        }
33        recordInteraction(inputDelay, processingTime, paintDelay) {
34          const totalResponseTime = inputDelay + processingTime + paintDelay;
35          this.interactionRecords.push(totalResponseTime);
36
37          
38          if (this.interactionRecords.length % 50 === 0) {
39            this.interactionWindows.push([...this.interactionRecords]);
40            this.interactionRecords = []; 
41          }
42        }
43        calculate() {
44          if (
45            this.interactionRecords.length === 0 &&
46            this.interactionWindows.length === 0
47          ) {
48            console.warn("未检测到任何交互记录");
49            return null;
50          }
51
52          
53          const windowResults = this.interactionWindows.map((window) => {
54            if (window.length === 0) return 0;
55
56            
57            const sortedWindow = [...window].sort((a, b) => a - b);
58            const trimmedWindow = sortedWindow.slice(0, -1); 
59
60            return this.calculatePercentile(trimmedWindow, 75);
61          });
62
63          
64          let remainingResult = 0;
65          if (this.interactionRecords.length > 0) {
66            const sortedRemaining = [...this.interactionRecords].sort(
67              (a, b) => a - b
68            );
69            
70            const ignoreCount = Math.ceil(sortedRemaining.length / 50);
71            const trimmedRemaining = sortedRemaining.slice(0, -ignoreCount);
72            remainingResult = this.calculatePercentile(trimmedRemaining, 75);
73          }
74
75          
76          const allResults = [...windowResults];
77          if (remainingResult > 0) allResults.push(remainingResult);
78
79          return allResults.length > 0
80            ? this.calculatePercentile(allResults, 75)
81            : null;
82        }
83
84        calculatePercentile(data, percentile) {
85          if (data.length === 0) return 0;
86          const index = Math.ceil((data.length * percentile) / 100) - 1;
87          return data[Math.max(0, Math.min(index, data.length - 1))];
88        }
89      }
90      
91      const inpCalculator = new INPCalculator();
92
93      function simulateUserInteraction() {
94        
95        const inputDelay = Math.random() * 100;
96
97        
98        const processingTime = Math.random() * 200;
99
100        
101        const paintDelay = Math.random() * 50;
102
103        
104        inpCalculator.recordInteraction(inputDelay, processingTime, paintDelay);
105
106        console.log(
107          `[模拟] 交互延迟: ${inputDelay.toFixed(2)}ms, ` +
108            `处理耗时: ${processingTime.toFixed(2)}ms, ` +
109            `绘制延迟: ${paintDelay.toFixed(2)}ms, ` +
110            `总响应时间: ${(inputDelay + processingTime + paintDelay).toFixed(
111              2
112            )}ms`
113        );
114      }
115      
116      document.querySelectorAll(".item").forEach((item) => {
117        item.addEventListener("click", simulateUserInteraction);
118      });
119      
120      setInterval(() => {
121        const inpValue = inpCalculator.calculate();
122        if (inpValue !== null) {
123          console.log(`[INP计算结果] 当前INP值: ${inpValue.toFixed(2)}ms`);
124        }
125      }, 5000); 
126
127      
128      window.addEventListener("beforeunload", () => {
129        const finalINP = inpCalculator.calculate();
130        if (finalINP !== null) {
131          console.log(`[最终INP结果] ${finalINP.toFixed(2)}ms`);
132          
133        }
134      });
135    </script>
136  </body>
137</html>

(二)使用 Web Vitals 库

 1import { onINP } from 'web-vitals';
 2
 3onINP(console.log);

结尾

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

个人笔记记录 2021 ~ 2025