以用户为中心的性能指标
用户体验 | 描述 |
---|---|
是否发生? | 导航是否成功启动?服务器是否有响应? |
是否有用? | 是否已渲染可以与用户互动的足够内容? |
是否可用? | 用户可以与页面交互,还是页面仍在忙于加载? |
是否令人愉悦? | 交互是否顺畅而自然,没有滞后和卡顿? |
是否发生
first paint / first contentful paint
first paint: 首个元素绘制的时间。
first contentful paint: 首个内容绘制时间,具体指图片或者文本的首个像素渲染。
从定义上这两个指标定义上有所不同,但目前获取的数据却是一致的,我们可以把这两个指标看成我们平时所说的白屏时间。
计算方式
计算方式通过性能对象performance
来实现,后面的很多个指标都是利用该对象来获取的,它保存了页面中每一个http请求统计信息。点击这里了解更多关于performance对象。 这个对象在大部分浏览器都支持了,除了IE。
var paintEntries = performance.getEntriesByType('paint');
for (var i = 0; i < paintEntries.length; i++) {
var entry = paintEntries[i]
console.log('entry name:', entry.name)
console.log('spent time', entry.startTime + entry.duration)
}
是否有用
first meaningful paint
第一个有意义的元素渲染时间,有意义指的就是首屏重要元素,所以可以理解为首屏重要元素的渲染,当这个元素渲染出来后,则说明了我们的页面可用了。
计算方式
由于每个页面重要元素不甚相同,所以这个需要用户打点来收集,分别在最开始的位置和重要元素渲染后分别打点。
// 在head的最前面打开始的点
performance.mark('start fmp')
// 在重要元素渲染后打结束的点
performance.mark('end fmp')
// 计算first meaningful paint时间
var entry = performance.measure('fmp', 'start fmp', 'end fmp')
console.log('spent time', entry.startTime + entry.duration)
关于开始打点的位置,我们可以在head的最前端添加script代码,作为开始的点。
关于重要元素渲染后的打点,如果我们用react/vue,我们可以把这个重要元素提取为一个组件,然后在componentDidMount中打点。如果是vue,则在mounted方法打点。如果是普通html,则可以在重要到元素下面添加script,然后script里面打点。
如果图片是重要元素,则可以计算首图时间来作为fmp,后面会讲到。
首图加载时间
首图时间是页面中首张图片的加载时间,计算首屏时间的时候,如果首屏中有图片,则首屏时间就是首图时间。
计算方式
计算首图时间,我们同样可以利用performance
来获取图片加载详情。我们需要标记哪些图片是首图,这里我们给首图元素(不一定是img元素)加个perf-img=”true”来标示首图元素,可以多个。
// html
<img src={logo} width={224} perf-img="true"/>
// js
// 获取首图元素的图片地址,首图元素可能是img/元素的background
function getImgSrc(dom) {
var imgSrc;
if (dom.nodeName.toUpperCase() == 'IMG') {
imgSrc = dom.src;
} else {
var computedStyle = window.getComputedStyle(dom);
var bgImg = computedStyle.getPropertyValue('background-image') || computedStyle.getPropertyValue('background');
var matches = bgImg.match(/url\(.*?\)/g);
if (matches && matches.length) {
var urlStr = matches[matches.length - 1]; // use the last one
var innerUrl = urlStr.replace(/^url\([\'\"]?/, '').replace(/[\'\"]?\)$/, '');
if (((/^http/.test(innerUrl) || /^\/\//.test(innerUrl)))) {
imgSrc = innerUrl;
}
}
}
return imgSrc;
}
var entries = performance.getEntriesByType('resource');
for (var i = 0; i < paintEntries.length; i++) {
var entry = entries[i];
if (entry.initiatorType === 'img') {
var $mainImgs = document.querySelectorAll('[perf-img]');
var len = $mainImgs.length;
for (var i = 0; i < len; i++) {
var $mainImg = $mainImgs[i];
// 如果加载的是首图图片
if (entry.name === getImgSrc($mainImg)) {
console.log('spent time', entry.startTime + entry.duration)
}
}
}
}
是否令人愉悦
long task
js是单线程的,所有的任务都需要放在主线程的队列中执行,浏览器的ui任务和js任务都需要放在队列中等待主线程空闲后执行,如果js任务耗时较长,则会导致ui无法渲染,用户无法和页面交互,用户就会感知到页面滞后或者卡顿。获取long task需要利用PerformanceObserver
对象,这个对象可以监控long task。
计算方式
var longTaskObserver = new PerformanceObserver((list) => {
var entries = list.getEntries();
for (var i = 0, len = entries.length; i < len; i++) {
var entry = entries[i];
var time = entry.startTime + entry.duration;
console.log('long task spent', time);
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
是否可用
time to interactive
简称tti,可交互时间,表示用户是否可以通过点击、输入等和页面交互。用户无法和页面进行交互到原因有大概有这几个: 1、渲染页面/组件的js未加载
2、js任务耗时长
3、加载页面/组件需要的接口阻塞
计算方式
谷歌提供了ttiPolyfill的sdk来计算(实际上它也是计算long task),但它不适用于第三点, 而貌似第三点很普遍存在,拉接口-渲染数据。 所以我们需要有个可靠的方法来计算。如果我们仔细想想,计算tti可以用和计算fmp一样的方法,我们把有意义的元素换成可交互的元素,然后在该元素渲染后打点,虽然需要手动打点,但比较可靠。
其他
首api(重要api)加载时间 / 首js(重要js)加载时间 / 首css(重要css)加载时间
这几个指标同样重要: 如果重要api未加载下来,页面可能无法显示,则页面“不可用”-是否可用。
如果重要js未加载下来,页面就是白屏,则页面“没发生”-是否发生。
如果重要css未加载下来,页面没有样式,则页面“不可用”为否-是否可用。
计算方式
这几个指标的计算方式同首图的计算方式一样,通过performance获取resource类型的entry即可。
var __performance = {
firstApi: 0,
firstCss: 0,
firstJs: 0,
firstImg: 0
}
var entries = performance.getEntries('resource');
for (var i = 0, len = entries.length; i < len; i++) {
var entry = entries[i];
const time = entry.startTime + entry.duration;
switch (entry.initiatorType) {
case 'xmlhttprequest':
if ("/api/v1/users/profile" === entry.name || "/api/v1/users/status")) {
// 直接覆盖上个的值,至于为什么,下面会提到
__performance.firstApi = time;
}
break;
case 'img':
var $mainImgs = document.querySelectorAll('[perf-img]');
var len = $mainImgs.length;
for (var i = 0; i < len; i++) {
var $mainImg = $mainImgs[i];
if (entry.name === getImgSrc($mainImg)) {
__performance.firstImg = time;
}
}
break;
case 'link':
if (https://tech.souyunku.com/app.*\.css/.test(entry.name)) {
__performance.firstCss = time;
}
break;
case 'script':
if (https://tech.souyunku.com/app.*\.js/.test(entry.name)) {
__performance.firsJs = time;
}
break;
}
}
同个指标多个值问题
对于首图、首api、首js、首css,都可能包含多个,我们以首api为例。
如果first api有多个,那么以哪个为准还是两个的值叠加呢?这分两种情况讨论:
1、多个并行请求,我们以耗时最长的请求为准。 2、多个请求串行,则应该多个请求的时间叠加合。
对于第一种情况,我们获取数据的方法很简单,以请求耗时最长的为准:
对于第二种情况,我们需要叠加两个请求的startTime+duraton吗?实际上不需要,PerformanceObserver帮我们做了这个工作,第二个请求的startTime=第一个请求耗时,所以我们获取指标的时候还是这样:
var time = entry.startTime + entry.duration
最后以time最大的值为准。
何时获取&上报数据
最理想的情况就是当页面稳定的时候获取然后上报数据,这样可以获取到全面的数据。但是页面稳定的时候是个无解的问题,我们只能在页面onload后取个合适的时间,这个时间建议5s,如果5s后页面还没稳定,用户就会觉得卡顿,这和用户感知相关。
用户感知长度表:
用户感知 | 响应时间 |
---|---|
流畅 | < 1s |
可用 | 1s ~ 2s |
丢帧 | 纯页面性能指标 |
卡顿 | 3s ~ 5s |
阻塞 | > 5s |
function report() {
setTimeout(function() {
// 利用上面的代码获取到各个指标后这里获取
console.log(window.__performance)
}, 5000)
}
if (document.readyState == 'complete') {
report();
} else {
window.addEventListener('load', () => {
report();
});
}
对于如何判断页面5s后某些指标仍然未获取得到,则判定为页面有阻塞:
1、 fp/fcp 5s后为0(一直白屏)
2、 首配置了首js,5s后仍然为0(说明5s后js未加载下来)
3、 如果配置了首api,5s后仍然为0(说明5s后接口还未拉取回来)
4、 如果手动打点fmp,5s后仍然为0或者fmp耗时超过5s(重要元素渲染超过5s)
5、 如果手动打点了tti,tti耗时超过5s(页面到可响应时间超过5s)
6、 long task超过5s(js阻塞超过5s)
如果判定页面阻塞,则5s后我们获取到的数据就是超时。
最后附上源码:
源码