这篇文章的主题是“怎么收集并上报页面的性能数据?”,主要目的是分享一下笔者是怎么实现它的。
在这个时代,似乎开发者们都不再关心页面的性能。大家会认为随着网络速度越来越快,用户端设备性能越来越好,只要做好基础设施的搭建,比如脚本的打包,传输时压缩等等,我们就可以不再关心页面的性能。不能否认的是我们的网络和设备,甚至开发使用的基础设施都在变好,但是用户和我们开发时使用的环境并不都是一样的,开发时的环境往往比用户的会好很多。而且即使平均状况都不错,但“好”的比例是多少,“不好”的比例又是多少?我们并不知道。和业务挂钩的话,比如成交率,“好”的成交率是多少,“不好”的成交率又是多少?因为我们都不知道,所以更需要解除这个疑惑,而解除的方法就是收集上报页面的性能数据。
那什么时候开始做这件事情?我的建议是在你的页面上线时就应该做好这件事。因为即使“业务的快速迭代”在初期看来比页面性能更重要,但是收集上报数据这件事的成本并不高啊。看完这篇文章你会有一个自己的判断。
我们先来拆解一下需求,比较容易理解的是可以将需求分为三部分 — 收集、上报和性能数据。所以我们先来说说性能数据,它是什么?它从哪儿获取?
什么是性能数据?
什么是性能数据,我们可以理解为它用来描述页面的行为表现和状态,比如我们会说页面加载地特别快,这个页面运行地特别顺畅、一点都不卡,这是页面的行为表现;又比如说我们的页面打开时一片空白,点了按钮卡死在那了,“空白”、“卡死了”都是在说页面的状态怎么样。所以性能数据应该能够告诉我们,页面的行为表现和状态是怎么样的。
W3C 为我们的浏览器制订了一系列的标准,这些标准推荐浏览器应该暴露哪些性能相关的 API,浏览器应该提供哪些信息来告诉开发者他们的页面性能表现怎么样,页面当时所处的状态等等。
比如 W3C 最早定义了 Navigation Timing、Resource Timing 和 User Timing 分别用来描述页面的导航、资源请求、脚本运行时序相关的信息。如果你想知道 W3C 一共制定了哪些性能相关的标准和它们目前的状态可以在这里查看:webperf-dashboard。
上面说了标准定义了 Navigation Timing、Resource Timing 和 User Timing 这些性能相关的信息,那我们从哪里能够获取到它们呢?Performance Timeline 标准就定义了这样一个接口,用来统一获取页面性能相关信息的方式。它在 performance 对象中提供了 getEntries() 方法来获取这些信息。代码示例:
const perfEntries = performance.getEntries();
for (let i = 0; i < perEntries.length; i++) {
console.log(`
Name: ${perfEntries[i].name}
Entry Type: ${perfEntries[i].entryType}
Start Time: ${perfEntries[i].startTime}
Duration: ${perfEntries[i].duration}
`);
}
需要注意的是,getEntries 目前并不支持任何参数,部分文档中的内容可能会造成误导。如果需要通过条目的 name 或 entryType 来筛选,可以使用 getEntriesByName
或者 getEntriesByType
方法。代码示例:
// 筛选出页面中资源请求名为 ‘https://www.xxx.com/get.json’ 的条目
const resourceEntries = performance.getEntriesByName(
‘https://www.xxx.com/get.json’,
‘resource’,
);
通过 caniuse 可以查到 getEntries 兼容性已经到了可用的状态,大部分现代浏览器都已经支持这个 API。部分不兼容的浏览器我们可以做降级处理,比如可以通过已废弃的 performance.timing
和 performance.navigation
来获取 Navigation Timing 导航时序的信息。代码示例:
if (!’getEntries’ in performance) {
perfEntries.push(pefromance.timing);
}
如果你想知道使用 getEntries() 能获取到哪些类型的性能条目?我们可以在这里查看:Timing Entry Names Registry。如上图所示,它列出了 11 种条目类型,每种条目都包含一个或多个页面性能指标。其中 ‘ availabelFromTimeline’ 一列值为 ‘true’ 表示可以通过 getEntries 方法获取到的条目类型,如果为 ‘false’ 则需要通过 PerformanceObserver 来监测。我们先来过一下这些类型的条目分别包含什么数据:
- mark:它包含开发者通过调用 performance.mark 标记的信息。
- measure: 它包含开发者通过调用 perfromance.measure 测量的数据。
- navigation:它包含页面导航相关的时序信息,包括下图中标记的这些时间点。
- resource:它包含页面中资源加载的时序信息,包括下图中标记的这些时间点数据。
- longtask:它包含任务队列中执行时间 >50 ms 的任务,这样的任务被称为“长时任务”。
- paint:它包含页面渲染性能相关的指标,比如FP(首次渲染时间)和FCP(首次内容渲染时间)。
- element:如果元素设置了属性“elementtiming”,浏览器会记录这些元素的加载和渲染时间,开发者可以通过监听这类条目来获得这些信息。
- event:它用来监测浏览器输入事件的延迟时间。
- first-input:它包含页面首次发生的输入事件的延迟信息。
- layout-shift:它包含页面加载时稳定度相关的信息,具体信息可以查看:https://github.com/WICG/layout-instability。
- largest-contentful-paint: 它包含页面的LCP(最大内容元素渲染时间)。
上面列举的条目类型中,‘longtask’, 'element’, ‘event’, ‘layout-shift’ 和 ‘largest-contentful-paint’ 类型的条目只能通过 PerformanceObserver 对象监测并获取。那么 PerfromanceObserver 该怎么用呢?直接上代码:
// 设置监听器回调,当有新的条目被记录时,回调函数都会被调用
const ob = new PerformanceObserver((list) => {
if (list.getEntries().length) {
perfEntries.push(list.getEntries())
}
});
ob.observe({ entryTypes: [‘longtask’, ‘element’, ‘event’, ‘layout-shift’, ‘largest-contentful-paint’] });
上报时机
解决了数据内容和来源的问题,我们来看一下该何时上报我们收集的数据?最直接的方式是在 onload 时进行上报,对于页面是后端直出的情况是适用的。代码示例:
// 在页面 load 时发起上报程序
window.onload = () => {
upload();
}
但大多 SPA 应用可能都不适用,因为可能有一些数据请求是在前端发起的,所以需要等待请求返回之后才能完成页面的渲染。这时候另外一个选择就是在页面卸载时进行上报。但这会带来一些问题,比如:
- 可能会丢失数据,请求还没完成浏览器就取消掉了;
- 如果使用同步接口,会阻塞页面的卸载;
- 浪费用户资源,因为很多数据并不是我们需要的。
所以我们再回到最基本的需求上,我们想要收集的是哪些数据,大多时候我们需要收集的是页面首屏渲染相关的数据。所以我们要找的时机就是首屏渲染完成的时候,这对于 SPA 应用来说可能是个难题。 但我们可以换个思路想,既然 SPA 应用大多时候需要等待请求完成后才会完成首屏渲染,那么是不是可以在所有请求完成后进行上报?除了手动上报之外,还有其他方法可以知道请求都完成了吗?我们可以利用上面讲到的 PerformanceObserver,它可以监听到有没有新的请求条目,如果在足够长的时间内没有获取到新的请求条目,是不是就可以认为资源都已经加载完成了?这在大多数时候是可靠的。代码示例:
const ob = new PerformanceObserver((list) => {
if (list.getEntries().length) {
const timer = setTimeout(() => {
// 如果在一定时间内没有再获取到资源请求的条目
// 则发起上传程序
upload();
}, 500); // 根据应用情况设置足够长的时间
clearTimeout(timer);
}
});
ob.observe({ entryTypes: [‘resource’] })
上面说的都是在页面能够成功加载完成的情况,但有时候页面加载时间过长用户直接关闭了页面,这时候我们就无法上报这些“放弃加载”的性能数据,我们统计的数据也无法反应出“放弃加载”的情况。所以我们还需要在页面卸载时发起上传程序。代码示例:
window.onunload = () => { upload() }
如何上报?
我们来看最后一步,如何上报我们收集的页面性能数据?我们可以通过 fetch 或者 XMLHttpRequest 接口,但是因为可能要在页面卸载回调中执行上报程序,所以需要使用同步的接口来实现。可供选择的接口,除了同步的 XMLHttpRequest 以外,我们还可以使用 navigator.sendBeacon 方法来发送我们收集的数据。代码示例:
function sendData(uploadData) {
if (!sendDataByBeacon(uploadData)) {
sendDataByXHR(uploadData);
}
}
function sendDataByBeacon(uploadData) {
if (’sendBeacon' in navigator) {
// 发送成功会返回 true
return navigator.sendBeacon(uploadData));
}
return false;
}
function sendDataByXHR(uploadData) {
const xhr = new XMLHttpRequest();
// 第三个参数设为 false,表示发起同步请求
xhr.open(‘POST’, url, false);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
finished = true;
}
};
xhr.send(uploadData);
}
尾声
到此为止,我们就实现了一个用来收集上报性能数据的工具。所以我们再回到一开始的那个问题,我们应该什么时候开始做这件事?相信读者已经有了自己的判断。 如果文中有什么错误烦请指出,感谢!也欢迎留言讨论和交流。