来来来,前端性能监控,带你拿到正确的性能指标
前言
作为前端开发,我们有必要关注我们各个客户端的性能指标,比如白屏时间,最早可交互时间等。 通过这些性能指标的监控,就能知道我们当前页面性能如何,与竞品相比有哪些差距。 尤其是c端的商品详情页等,用户对于这类页面容忍度较低,慢个一两秒用户可能就已经划走了,因此对前端性能进行监控十分重要。
前端性能监控
那么如何进行前端性能监控呢?通常我们会使用时间去表示当前的页面性能比如白屏时间,首屏时间。接下来将介绍如何获得这些关键节点的时间值。
performance介绍
首先来介绍performance这个是挂载在window下的一个对象,上面暂存了页面很多关键节点的数据。 不妨打印看看里面都有什么,以掘金为例:

里面存储了很多字段,其中timing 存储了核心数据下面我们逐个解释下。
memory (内存)
可以获得当前页面的内存使用情况。
- jsHeapSizeLimit (表示当前页面最多可以获得的 JavaScript 堆大小)
- totalJSHeapSize (表示当前页面已经分配的 JavaScript 堆大小)
- usedJSHeapSize (表示当前页面 JavaScript 已经使用的堆大小)
理想情况来说,可以通过持续上报jsHeapSizeLimit
和 totalJSHeapSize
去监控当前页面内存使用情况,是否已用完内存等,如totalJSHeapSize
大于jsHeapSizeLimit
会触发页面崩溃,存在内存泄露的风险。
注:已弃用,目前仍有浏览器支持,已从相关的 web 标准中移除,使用的时候需要慎重。

navigation
可以获得当前页面的进入方式,重定向次数等信息。
- redirectCount,重定向的数量,经过多少次重定向进入这个页面 (注:这个页面有同源限制,只能统计同源情况下)
- type,进入页面的方式
- 0,正常进入非刷新,非重定向
- 1,通过刷新的方式进入
- 2,通过前进回退按钮进入
- 255,非是上述情况
timing (网络请求、页面解析等核心时间节点数据)
下面来介绍下上述字段的含义:
- navigationStart:表示上一个文档卸载结束时的unix时间戳,如果没有上一个文档,则等于fetchStart。
- unloadEventStart:表示前一个网页(与当前页面同域)unload的时间戳,如无前一个网页unloade或前一个网页与当前不同域,则为0。
- unloadEventEnd: 返回前一个unload时间绑定的回调执行完毕的时间戳。
- redirectStart:前一个Http重定向发送时的时间。有跳转且是同域名内重定向,否则为0。
- redirectEnd:前一个Http重定向完成时的时间。有跳转且是同域名内重定向,否则为0。
- fetchStart:浏览器准备使用http请求文档的时间,在检查本地缓存之前。
- domainLookupStart/domainLookupEnd:DNS域名查询开始/结束的时间,如果使用本地缓存(则无需DNC查询)或持久链接,则和fetchStart一致。
- connectStart:HTTP(TCP)开始或重新建立链接的时间,如果是持久链接,则和fetchStart一致。
- connectEnd:HTTP(TCP)完成建立链接的时间(完成握手),如果是持久链接,则和fetchStart一致。
- secureConnectionStart:Https链接开始的时间,如果不是安全链接则为0。
- requestStart:http在建立链接之后,正式开始请求真实文档的时间,包括从本地读取缓存。
- responseStart:http开始接收响应的时间(获取第一个字节),包括从本地读取缓存。
- responseEnd:http响应接收完全的时间(最后一个字节),包括从本地读取缓存。
- domLoading:开始解析渲染DOM树的时间。
- domInteractive:完成解析DOM树的时间。
- domContentLoadedEventStart:DOM解析完成后,页面内资源加载开始的时间。
- domContentLoadedEventEnd:DOM解析完成后,网页内资源加载完成的时间(如js脚本加载执行完)
- domComplete:DOM树解析完,资源也准备就绪。
- loadEventStart:load事件发送给文档,即load函数开始执行时。
- loadEventEnd:load函数执行完毕的时间。
!!! 基于上述数据内容,我们可以得到如下的执行顺序模块。 !!!
- 页面卸载部分
- 卸载:navigationStart、unloadEventStart、unloadEventEnd
- 网络部分
- 跳转:redirectStart、redirectEnd
- 请求文档:fetchStart
- DNS查询:domainLookupStart、domainLookupEnd
- 连接建立:connectStart、connectEnd
- 请求:requestStart、responseStart、responseEnd
- 页面解析部分
- 解析dom:domLoading、domInteractive
- 资源加载,dom渲染:domContentLoadedEventStart、domContentLoadedEventEnd、domComplete
前端性能数据获取
基于Performance输出简单的性能指标
基于上述参数指标,可以输出一段简单的性能监控代码。
const navigationType = {
0:'正常进入非刷新,非重定向',
1:'通过刷新的方式进入',
2:'通过前进回退按钮进入',
255:'非正常进入,非刷新,非前进回退进入'
}
let Performance = window.performance
let timing = Performance.timing
let navigation = Performance.navigation
let memory = Performance.memory
let PerformanceObj = {
timing: {},
navigation: {}
} // 性能监控对象
if(timing) {
PerformanceObj['timing']['上一页面的卸载耗时'] = timing.unloadEventEnd - timing.navigationStart
PerformanceObj['timing']['重定向耗时'] = timing.redirectEnd - timing.redirectStart
PerformanceObj['timing']['查询appDNS缓存耗时'] = timing.domainLookupStart - timing.fetchStart
PerformanceObj['timing']['DNS查询耗时'] = timing.domainLookupEnd - timing.domainLookupStart
PerformanceObj['timing']['TCP连接建立耗时'] = timing.connectEnd - timing.connectStart
PerformanceObj['timing']['服务器响应耗时'] = timing.responseStart - timing.requestStart // 发起请求到响应第一个字节
PerformanceObj['timing']['request请求耗时'] = timing.responseEnd - timing.responseStart // 响应第一个字节到响应最后一个字节
PerformanceObj['timing']['总耗时'] = (timing.loadEventEnd || timing.loadEventStart || timing.domComplete || timing.domLoading) - timing.navigationStart
PerformanceObj['timing']['解析dom树耗时'] = timing.domComplete - timing.responseEnd
}
if(navigation) {
PerformanceObj['navigation']['重定向次数'] = navigation.navigation || 0
PerformanceObj['navigation']['进入页面方式'] = navigationType[navigation.type] || '进入页面方式加载异常'
}
if(memory) {
setInterval(()=>{
console.log(memory.jsHeapSizeLimit, memory.totalJSHeapSize)
}, 300)
}
效果如下所示:

前端性能核心指标(TTFB、FP、FCP、LCP、TTI、DCL)
看着上面东西已经比较完备了,但我们还需要提交一些核心的性能指标包括,下面来介绍下如何获取TTFB、FP、FCP、LCP、TTI、DCL等参数。
TTFB
TTFB指代从资源的请求到响应第一个字节的时间跨度。 即红框内:
影响TTFB的长短的因素包括重定向时延,DNS查询时延,链接建立延迟,请求响应时延
计算TTFB的方式:
(1) 基于PerformanceObj: TTFB = responseStart - navigationStart 注: responseStart:http开始接收响应的时间(获取第一个字节),包括从本地读取缓存。 navigationStart:表示上一个文档卸载结束时的unix时间戳,如果没有上一个文档,则等于fetchStart。
(2) 基于PerformanceObserver:
// 推广监听navigation的方式,计算navigation到responseStart的差值
new PerformanceObserver((entryList) => {
const [pageNav] = entryList.getEntriesByType('navigation');
// TTFB = pageNav.responseStart
}).observe({
type: 'navigation',
buffered: true
});
(3) 基于web-vitals
import {onTTFB} from 'web-vitals';
console.log(onTTFB)
可以看到基于web-vitals也可以获得TTFB,相对来说代码会更简洁。
FP(白屏时间)
FP指代用户发起请求到浏览器开始渲染页面内第一个像素点所经过的时间。一般html解析结束之后就会开始触发。 白屏时间长短对用户体验影响极大,因此计算时尽量保证准确。!!! 其中影响因素包括重定向时延,DNS查询时延,链接建立延迟,请求响应时延
那我们如何获得FP的具体值呢? 基于控制台我们可以直接读取到FP的值。

计算FP的方法: (注:网上有很多计算FP的方式,但是得出的结果千奇百怪,读者在计算时最好去控制台比对一下,以官方标准为准)
// 开始渲染第一个节点的时间
performance.getEntries('paint').filter(entry => entry.name == 'first-paint')[0].startTime
FCP(首次内容绘制)
FCP指代测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。

不同于LCP,FCP只渲染了部分内容。
同样的基于控制台我们可以获得FCP,值得注意的是由于单页应用的广泛使用基于控制器的FCP已经不能满足我们的使用,因此在计算时需分情况处理。

计算FCP的方法:(获取控制台的FCP) (1) 基于Performance:
performance.getEntries('paint').filter(entry => entry.name == 'first-contentful-paint')[0].startTime;
(2) 基于PerformanceObserver:
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
// entry.startTime的值即为FCP
}
}).observe({type: 'paint', buffered: true});
(3) 基于web-vitals:
import {onFCP} from 'web-vitals';
console.log(onFCP)
注意:很多时候我们说的首屏时间也叫fcp,但是这个很多人指代一个页面渲染完成结果。
LCP (最大内容渲染时间)
LCP指代页面中最大的内容完成绘制的时间。
通常这类内容指代页面可视区域内,包括img,video,包含文本节点或其他行内块级元素子元素的块级元素等元素。
通常LCP的评定标准是基于元素在用户可视区域内的可见大小。
在计算LCP的过程中,仅仅计算最大且已经完成渲染的元素,如果后续出现一个更大的元素则会继续报告一个新的PerformanceEntry。但是那个更大的并不会替代前一个已经作为最大的元素节点,除非前一个已经作为最大的元素节点已经被移除。
计算LCP的方法:
(1) 基于PerformanceObserver:
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// entry.startTime 的值即为LCP
}
}).observe({type: 'largest-contentful-paint', buffered: true});
(2) 基于web-vitals:
import {onLCP} from 'web-vitals';
console.log(onLCP)
TTI (最早可交互时间)
TTI指代页面从加载资源到页面渲染,最早可以与用户进行完全可交互的时间,也可以用来描述页面的响应快慢,如果TTI过长容易被用户将页面当成已失败处理。
当页面存在有用的内容,可见元素的关联事件函数已绑定注册,事件函数可以在事件发生后50ms内执行,则可以认为当前页面已达到完全可交互的状态。
如何计算TTI?
计算追踪TTI比较困难,这里借助web.dev/ 上的描述来解释。
如需根据网页的性能跟踪计算 TTI,请执行以下步骤:
- 先进行FCP即首屏绘制。
- 沿时间轴正向搜索时长至少为 5 秒的安静窗口。(安静窗口:没有长任务且不超过两个正在处理的网络 GET 请求)
- 沿时间轴反向搜索安静窗口之前的最后一个长任务,如果没有找到长任务,则在 FCP 步骤停止执行。
- TTI 是安静窗口之前最后一个长任务的结束时间(如果没有找到长任务,则与 FCP 值相同。
计算TTI的方法:
通过监听第一个长任务去计算TTI
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.startTime 的值即为TTI
}
}).observe({entryTypes: ['longtask']});
DCL (DOMContentLoaded)
DCL指代当HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式,图像和子框架的完成加载的时间。 (注:DCL指代的是初始HTML文档解析完的时间,而load指代是页面资源加载时间,二者先后顺序不一定,如果初始HTML复杂则DCL在L后面,反之如果页面资源多则L在DCL后面)
计算DCL的方法: 监听DOMContentLoaded
document.addeventListener('DOMContentLoaded', function() {
+new Date() - window.performanceObser.timing.navigationStart
}, false);
总结
基于上述的描述我们可以基于此实现一个简单的数据上报代码,上报方法用send表示。 基础数据上报:
const navigationType = {
0:'正常进入非刷新,非重定向',
1:'通过刷新的方式进入',
2:'通过前进回退按钮进入',
255:'非正常进入,非刷新,非前进回退进入'
}
let Performance = window.performance
let timing = Performance.timing
let navigation = Performance.navigation
let memory = Performance.memory
let PerformanceObj = {
timing: {},
navigation: {}
} // 性能监控对象
if(timing) {
PerformanceObj['timing']['上一页面的卸载耗时'] = timing.unloadEventEnd - timing.navigationStart
PerformanceObj['timing']['重定向耗时'] = timing.redirectEnd - timing.redirectStart
PerformanceObj['timing']['查询appDNS缓存耗时'] = timing.domainLookupStart - timing.fetchStart
PerformanceObj['timing']['DNS查询耗时'] = timing.domainLookupEnd - timing.domainLookupStart
PerformanceObj['timing']['TCP连接建立耗时'] = timing.connectEnd - timing.connectStart
PerformanceObj['timing']['服务器响应耗时'] = timing.responseStart - timing.requestStart // 发起请求到响应第一个字节
PerformanceObj['timing']['request请求耗时'] = timing.responseEnd - timing.responseStart // 响应第一个字节到响应最后一个字节
PerformanceObj['timing']['总耗时'] = (timing.loadEventEnd || timing.loadEventStart || timing.domComplete || timing.domLoading) - timing.navigationStart
PerformanceObj['timing']['解析dom树耗时'] = timing.domComplete - timing.responseEnd
}
if(navigation) {
PerformanceObj['navigation']['重定向次数'] = navigation.navigation || 0
PerformanceObj['navigation']['进入页面方式'] = navigationType[navigation.type] || '进入页面方式加载异常'
}
// 上传基本指标
send('performance', PerformanceObj)
if(memory) {
setInterval(()=>{
// 上传内存使用情况
send('memory', memory.jsHeapSizeLimit, memory.totalJSHeapSize)
}, 300)
}
核心数据上报:
let coreObj = {}
let Performance = window.performance.timing
// TTFB
coreObj['TTFB'] = Performance.responseStart - Performance.navigationStart
// FP
coreObj['FP'] = performance.getEntries('paint').filter(entry => entry.name == 'first-paint')[0].startTime
// FCP
coreObj['FCP'] = performance.getEntries('paint').filter(entry => entry.name == 'first-contentful-paint')[0].startTime
// LCP
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if(!coreObj['LCP']) {
coreObj['LCP'] = entry.startTime
}
}
}).observe({type: 'largest-contentful-paint', buffered: true});
// TTI
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if(!coreObj['TTI']) {
coreObj['TTI'] = entry.startTime
}
}
}).observe({entryTypes: ['longtask']});
// DCL
document.addeventListener('DOMContentLoaded', function() {
coreObj['DCL'] = +new Date() - window.performanceObser.timing.navigationStart
}, false);
// 核心数据上报
window.onload = function() {
send('core', coreObj)
}
转载自:https://juejin.cn/post/7223280402475089978