前端性能监控是一个老生常谈的问题,但在实际使用过程中,我们更多接触的是监控SDK如何引入,如何去监控网站上查阅性能数据。那么,我们是否真的了解前端性能数据是如何采集的呢?
在Performance API出现之前,没有太好的手段对网站的性能进行监控,也无法分析出网站加载和解析具体哪个过程对性能影响最大,整体网站优化工作略显低效。
在W3C小组引入Performance API之后,网站的性能监控迎来了新的阶段。
什么是Performance API?
Performance API 是一组 JavaScript API,为开发人员提供了获取加载及解析网站的过程中关键时间节点的性能数据的能力。
-
提高性能:通过分析性能数据,开发人员可以识别出导致性能问题的应用程序的区域并对其进行优化。
-
更好的用户体验:更快的加载页面和应用程序可带来更好的用户体验。
目前为止,大部分浏览器版本都支持了Performance API。
Performance API 是如何工作的?
Performance API 通过测量诸如页面加载时间、DOM 准备时间、第一次绘制时间等各种性能指标来工作。
性能时间线
Prformance API测量的这些性能指标,在浏览器中会添加到使用了高精度时间戳 的**性能时间线(performance timeline)**中,这些性能时间线会在浏览器的开发者工具中展示,便于开发人员能够访问、检测和检索 Web 应用程序的整个生命周期中的各种性能指标。
高精度时间
性能时间线中的时间采用了高精度时间戳+单调时钟进行高精度测量。
-
高精度时间戳 以毫秒为单位,小数位会精确到 5 µs(微秒)。
- 部分浏览器不支持精确到微秒,则会精确到毫秒。
- 下面「扩展结构」中的startTime也是高精度时间戳
-
单调时钟:performance.now()方法和performance.timeOrigin只读值
-
由于常规的
Date.now()
受到系统时钟影响,所以数据无法单调增加。 -
performance.now()
,是高精度时间戳且单调递增,不受系统时钟影响 -
performance.timeOrigin
是performance.now()
的起始时间,两者之差为当前节点的持续时间。- 不同浏览器的上下文中,
performance.timeOrigin
取值不同 - window对象中,
performance.timeOrigin
是创建导航的时间 - Web Worker中,
performance.timeOrigin
是Worker创建的时间
- 不同浏览器的上下文中,
-
性能条目:PerformanceEntry
每一个性能指标都对应一个性能条目(PerformanceEntry),其作为单个数据记录在性能时间线中。
PerformanceEntry数据的结构
基础结构
在性能条目对象中,默认有以下属性:
扩展结构
在实际使用过程中,具体的指标条目数据都继承于基础结构,并对部分字段进行限定。下面将基于entryType分别进行讲述:
resource
-
基础数据
- name,加载资源的url。
- startTime,资源请求的时间戳,等同于
PerformanceResourceTiming.fetchStart
- duration,返回
PerformanceResourceTiming.responseEnd
和startTime的差值
-
扩展数据
-
资源加载的时间线,记录相关节点的时间戳
若跨域请求资源并且没有HTTP响应头未设置
Timing-Allow-Origin
,则这些时间也会为0。-
redirect*,表示页面重定向的开始或结束的时间
-
workerStart,Service Worker线程的运行时间
- 若Service Worker线程已经在运行,则记录其中FetchEvent事件前的时间
- 若Service Worker线程未运行,则记录该线程启动的时间
- 没有使用Service Worker线程,则为0
-
fetchStart,开始执行获取资源操作的时间
-
domainLookup*,表示DNS解析的开始或结束的时间
-
connect*,表示TCP连接的开始或结束的时间
- 若从浏览器缓存中获取数据,则connectStart为0
-
secureConnectionStart,建立TLS等安全链接的时间
- 未使用安全链接,则为0
- 若从浏览器缓存中获取数据,则为0
-
requestStart,浏览器开始从服务器、缓存或本地资源请求资源之前的时间
- 若从浏览器缓存中获取数据,则为0
-
【实验功能】firstInterimResponseStart,记录浏览器接收到服务器返回的第一个状态码为1xx的响应的时间
- 若从浏览器缓存中获取数据,则为0
-
response*,表示浏览器收到服务器响应的开始或结束的时间
-
-
initiatorType,用于判断以何种方法加载资源
- 用于区分图片、js、css、XHR、fetch
-
资源的大小
- encodedBodySize,解码前资源的大小(以byte为单位)。为0则可能为跨域请求失败
- decodedBodySize,解码前资源的大小(以byte为单位)。为0则可能为跨域请求失败
- transferSize,约为响应头字段+解码前资源大小。本地获取或跨域请求失败为0
-
【实验功能】responseStatus,请求资源返回的http状态码
- 跨域时可能有问题:Chrome中尝试本页面的资源数据,基本都是0
-
nextHopProtocol,获取资源的网络协议
-
renderBlockingStatus,表示阻塞页面渲染状态
-
【实验功能】deliveryType,标识资源是否从缓存或预加载中获取
-
serverTiming,记录服务器的计时指标。需要服务器发送
Server-Timing
标头
navigation
-
基础数据
-
name
由于当前文档仅一个,故性能条目数据中仅有一个
PerformanceNavigationTiming
对象,其name
属性为当前文档的URL。 -
startTime,固定为0
-
duration,返回
PerformanceNavigationTiming.loadEventEnd
(文档load事件)与startTime
的差值
-
-
扩展数据
-
实例对象为PerformanceNavigationTiming,继承于
resource
的实例对象 -
网页加载的时间线,记录相关节点的时间戳(经典老图)
-
除了
resource
中资源加载的时间的时间节点,还有一些独有时间节点: -
unloadEvent*,网页关闭的开始或结束时间
-
domInteractive,记录页面加载状态
document.readyState
为interactive
的时间- 该状态下,表示页面已完成文档加载并且文档已被解析,但脚本、图像、样式表和框架等子资源仍在加载
document.readyState
详细说明可以参考这里
-
domComplete,记录页面加载状态
document.readyState
为complete
的时间- 该状态下,表示页面文档和所有子资源已完成加载
-
domContentLoadedEvent*,表示页面文档DOMContentLoaded事件的开始或结束时间
- DOMContentLoaded在页面文档和延迟脚本下载并执行完毕后触发,不会等待样式、图片、异步脚本等加载
-
loadEvent*,页面文档load事件的开始或结束时间
- load事件,和DOMContentLoaded事件不同,会等待样式、图片等资源加载
-
-
initiatorType,固定为
navigation
-
redirectCount,非重定向导航以来重定向的次数
-
type,用于区分导航类型,可用于区分点击链接加载文档、刷新文档等操作
-
mark
- 基础数据
- name,创建时传入的参数(不要和PerformanceTiming的属性重复,不然会报错)
- duration,固定为0
- startTime,用于生成指标时间戳,默认为
performance.now()
measure
- 基础数据
- duration,表示两个Mark指标之间的时间间隔
- 更详细的参数及用法参考:performance.measure()
paint
-
基础数据
-
name
- first-paint,表示FP(首次渲染任何内容的时间)的指标数据
- first-contentful-paint,表示FCP(首次渲染文本或图像的时间)的指标数据
-
startTime,固定为0
-
duration,绘制的时间
-
-
扩展数据
largest-contentful-paint
-
基础数据
- name,固定为空
- duration,固定为0
- startTime,LargestContentfulPaint.renderTime || LargestContentfulPaint.loadTime
-
扩展数据
- 实例是LargestContentfulPaint
- element,监听的元素信息
- size,监听元素的宽高
- renderTime,图片渲染到屏幕的时间
- loadTime,图片加载到元素的时间
- url,若监听的图像元素,则返回图片的url;反之,则返回0
element
-
基础数据
-
name
- image-paint,图像元素绘制
- text-paint,文字元素绘制
-
duration,固定为0
-
startTime,PerformanceElementTiming.renderTime || PerformanceElementTiming.loadTime
-
-
扩展数据
- 实例是PerformanceElementTiming
- element,监听的元素信息
- url,若监听的图像元素,则返回图片的url;反之,则返回0
event
- 基础数据
-
name,返回事件的类型
- 可以通过
[...performance.eventCounts.keys()]
获取全部事件类型
- 可以通过
-
startTime,事件触发时间
-
duration,事件延迟时间,即事件触发时间到浏览器下次渲染的时间
- 默认情况下,当事件延迟超过104毫秒时,才会有
event
指标
- 默认情况下,当事件延迟超过104毫秒时,才会有
-
- 扩展数据
- 实例是PerformanceEventTiming
- interactionId,交互事件的唯一ID
- processingStart,事件触发后,浏览器开始执行的时间
- processingEnd,浏览器执行完事件的时间
- cancelable,事件能否被取消
first-input
- 基础数据
- 同
event
- 同
- 扩展数据
- 同
event
- 同
layout-shift
- 基础数据
- name,固定
layout-shift
- startTime,布局转变开始的时间
- duration,固定0
- name,固定
- 扩展数据
-
实例是LayoutShift
-
value,布局偏移得分(即LS分数),范围在0~1之间,用于计算CLS
- 具体的计算方式可参考这里。
-
【实验功能】lastInputTime,最近一次离散输入(如点击或按键)到此处布局偏移的时间
- 用于过滤可接受的用户交互导致的布局偏移
-
【实验功能】hadRecentInput,当lastInputTime小于500时,返回
true
;反之返回false
-
【实验功能】sources,返回布局偏移期间移动的Dom元素
-
longtask
-
基础数据
-
name,根据浏览器上下文环境返回不同的值
- 具体内容可以参考这里
-
startTime,长任务开始的时间
-
duration,长任务开始到结束的时间
-
-
扩展数据
- 实例是PerformanceLongTaskTiming
- 【实验功能】attribution,返回长任务涉及到的上下文信息(TaskAttributionTiming)
taskattribution
- 基础数据
- name,固定为
unknown
- startTime,固定为0
- duration,固定为0
- name,固定为
- 扩展数据
- 实例是TaskAttributionTiming
- containerType,长任务执行的上下文类型,为
iframe
、embed
或object
。无法确定则返回window
- containerSrc,上下文的src属性
- containerName,上下文的name属性
visibility-state
- 基础数据
- name,
visible
或hidden
- startTime,状态变动的时间
- duration,固定为0
- name,
- 扩展数据
PerformanceEntry数据的生成
-
大部分情况下,性能条目数据的生成不需要额外操作。比如在图片、script、css 等资源加载的时候,会自动生成对应的性能条目数据。
-
此外,可以通过PerformanceMark和PerformanceMeasure方法自定义指标条目数据。
-
PerformanceMark
会创建一个entryType
为mark
的指标数据,记录当前的时间戳(字段的详细说明见下文)。 -
PerformanceMeasure
则要复杂一些,会创建一个entryType
为measure
的指标数据,可以用于计算两个Mark指标之间的时间间隔。
-
PerformanceEntry数据的获取
目前有多个对象方法可用于获取性能条目数据。
Performance方法
-
获取所有性能条目数据:performance.getEntries()
-
获取指定entryType的性能条目数据:performance.getEntriesByType(${entryType})
-
获取指定name的性能条目数据:performance.getEntriesByName(${name})
PerformanceObserver对象监听
在生成的PerformanceObserver对象中传入回调函数,当新生成一个性能条目数据时会执行该回调函数。下面是监听resource性能条目数据的例子:
TypeScript
function callBackObserver(list, observer) {
const resourceList = list.getEntries().filter(item => item.entryType === "resource")
}
const observer = new PerformanceObserver(callBackObserver);
上面生成的回调函数,会监听所有的性能条目数据。若只需要部分entryType的性能条目数据,可以通过配置,控制仅监听某些entryType数据。
TypeScript
observer.observe({ entryTypes: ["resource"] })
若想获取之前已经生成的性能条目数据,需要在observe
方法中设置buffered
为true
。
TypeScript
observer.observe({ type: "resource", buffered: false });
PerformanceObserver对象的更多配置及方法可参考这里。
性能条目数据的数量限制
为了减少性能条目数据对浏览器内存的消耗,不同类型的性能条目数据有对应的数量限制(详情可见这里)。
当性能条目数量已经到达阀值时,浏览器会丢弃超出的性能条目数据。在PerformanceObserver对象的回调函数中,会传入droppedEntriesCount,用于标记具体丢弃的性能条目数量。
此外,针对entryType为resource的性能条目数据,会有一些额外的功能:
- 可以通过performance.setResourceTimingBufferSize方法来设置阀值。
JavaScript
// 设置最多保存500条resource性能条目数据
performance.setResourceTimingBufferSize(500)
- 也可以通过resourcetimingbufferfull事件来监听性能数据数量是否已达到阀值。
JavaScript
addEventListener("resourcetimingbufferfull", (event) => {});
onresourcetimingbufferfull = (event) => {};
其他性能指标数据:Performance
除了上述的性能数据,Performance对象中也有部分只读属性,用于记录性能数据。
-
performance.eventCounts
- entryType为
event
的性能条目数据中有过描述,用于返回可监听的事件类型
- entryType为
-
【废弃】performance.memory
- 返回当前网页的内存信息,目前已废弃,建议使用performance.measureUserAgentSpecificMemory()
-
【废弃】performance.navigation
- 网页的navigation信息,目前已废弃,建议使用entryType中的
navigation
性能条目数据
- 网页的navigation信息,目前已废弃,建议使用entryType中的
-
【废弃】performance.timing
- 网页加载及解析关键节点的时间,目前已废弃,建议使用entryType中的
navigation
性能条目数据
- 网页加载及解析关键节点的时间,目前已废弃,建议使用entryType中的
-
performance.timeOrigin
- 上文提到的性能时间线的高精度时间戳
我们如何使用Performance API?
经过上面的铺垫,再来看开篇的问题「如何获取前端性能数据」,就变得简单起来。
注:这里就不再展示解释这些前端性能及如何优化,仅仅介绍如何去计算这些性能数据。
页面性能
目前页面性能指标一般为CWV、FCP等数据。
CWV即** LCP、FID 、CLS三个指标,分别对应页面的加载性能、交互性和视觉稳定性**。
LCP
JavaScript
new PerformanceObserver((entryList) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1]; // Use the latest LCP candidate
console.log("LCP:", lastEntry.startTime);
})
.observe({type: 'largest-contentful-paint', buffered: true});
entryType为largest-contentful-paint的性能条目数据会有多个,当浏览器找到新的最大内容时,它会创建一个新性能条目。所以,LCP需要取list.getEntries()中的最后一个数据。
当发生滚动或输入事件时,浏览器会停止搜索更大的内容,因为这些事件可能会在网站上引入新内容。
FID
JavaScript
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const delay = entry.processingStart - entry.startTime;
console.log('FID is:', delay);
}
}).observe({type: 'first-input', buffered: true});
entryType
为largest-contentful-paint
的性能条目数据仅有1个,故FID数据仅会记录一次。
CLS
CLS的计算相对复杂,一般来说可以为会话时段得分的最大值。
JavaScript
new PerformanceObserver(entryList => {
let clsValue = 0; // cls分数
let sessionValue = 0; // 会话时段的cls值
const sessionEntries = []; // 会话时段的性能条目
for (const entry of entryList.getEntries()) {
// 过滤可接受的用户交互
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry =
sessionEntries[sessionEntries.length - 1];
/**
* 判断是否为一个会话时段:
* 1. 性能条目与上一条目的相隔时间小于1秒
* 2. 与会话中第一个性能条目的相隔时间小于5秒
**/
if (
sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// 更新CLS
if (sessionValue > clsValue) {
clsValue = sessionValue;
console.log('CLS is:', clsValue);
}
}
}
}).observe({ type: 'layout-shift', buffered: true });
注意:在获取layout-shift性能指标数据时,需要过滤可接受的用户交互。
实际情况中,CLS的获取逻辑更加复杂,需要考虑网页在后台运行等情况,目前web vitals库
已实现这些特殊场景的处理,详情可以参考这里。
FCP
JavaScript
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP is:', entry.startTime);
}
}
}).observe({ type: 'paint', buffered: true });
加载资源性能
常见计时指标
按照上图中的resource
性能条目中的时间节点,进行差值计算:
- 测量 TCP 握手时间 (
connectEnd
-connectStart
) - 测量 DNS 查找时间 (
domainLookupEnd
-domainLookupStart
) - 测量重定向时间 (
redirectEnd
-redirectStart
) - 测量临时请求时间 (
firstInterimResponseStart
-requestStart
) - 测量请求时间 (
responseStart
-requestStart
) - 测量 TLS 协商时间 (
requestStart
-secureConnectionStart
) - 测量获取时间(无重定向)(
responseEnd
-fetchStart
) - 测量 ServiceWorker 处理时间 (
fetchStart
-workerStart
)
调用接口时长性能
- 筛选
resource
性能条目中中initiatorType
为fetch
或xmlhttprequest
的数据 - 通过
responseStart
-requestStart
计算接口请求时长
JavaScript
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (['fetch', 'xmlhttprequest'].includes(entry.initiatorType)) {
console.log('接口时长 is:', entry.responseStart - entry.requestStart);
}
}
}).observe({ type: 'resource', buffered: true });
其他场景
- 检查资源是否被压缩(
decodedBodySize
和encodedBodySize
不为空,且不相等) - 检查资源是否命中本地缓存(
decodedBodySize
不为空,且transferSize
为0
) - 检查是否使用现代且快速的协议(
nextHopProtocol
应该是 HTTP/2 或 HTTP/3) - 检查正确的资源是否是渲染阻塞的 (
renderBlockingStatus
)
加载页面性能
navigation
的实例对象继承于resource
实例对象,除了resource
中的计时指标外,还有一些独有的计时指标:
- 测量 DOMContentLoaded事件时间 (
domContentLoadedEventEnd
-domContentLoadedEventStart
) - 测量 Load事件时间 (
loadEventStart
-loadEventEnd
) - 测量 页面关闭事件时间 (
unloadEventEnd
-unloadEventStart
)
总结
上文介绍了Performance API的方法及数据,重点介绍了各种性能条目数据及其针对的场景。并且,简单分析了如何使用Performance API的能力,来帮助开发人员实现网页精细化性能优化。后续也会持续关注Performance新功能,及其对前端性能优化的帮忙。