前端应用的性能和异常监控对于确保应用质量和用户体验至关重要。随着 Web 应用的复杂性和用户期望的增加,开发人员需要有效地监控应用的运行情况,并及时采取措施来提高应用的性能和稳定性。
提升性能指标对于改善用户体验有很大意义。从页面开始加载到完整展示的背后会经历与服务器连接、页面加载、展示资源等很多环节,任意一个环节的卡顿都会影响网页的使用效果。我们可以借助 Performance API 获取性能指标数据。
比如想要获取页面加载相关数据,可以主动调用 performance.getEntriesByType('navigation'),返回的指标如下图所示,指标之间做减法即可算出每个阶段的耗时:
再比如获取浏览器渲染相关的指标,如 First Contentful Paint、Largest Contentful Paint、First Input Delay 等。以 First Input Delay 为例,它是指从用户首次与网页互动到浏览器实际能够开始处理事件以响应该互动的时间。
页面加载性能指标示意
这里我们用被动监听的方式获取它:
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log(entry);
}
}).observe({ type: "first-input", buffered: true });
打印出的结果如下,可以看到触发类型是 pointerdown 即点击鼠标,触发时间为页面加载完三秒多:
{
"name": "pointerdown",
"entryType": "first-input",
"startTime": 3566.5,
"duration": 64,
"processingStart": 3613.7000000029802,
"processingEnd": 3613.7999999970198,
"cancelable": true
}
更省事的方法是用web-vitals库,优点是进行了封装和兼容处理:
import { onFID } from 'web-vitals';
// Measure and log FID as soon as it's available.
onFID(console.log);
异常监控
2.1 异常类型
TypeError、SyntaxError 等异常:可以通过 window.onerror 和 window.addEventListener('error', callback) 捕获,回调函数中能获取到异常信息、URL、行列号和堆栈。也可以通过 try catch 手动捕获异常后收集。
window.onerror = (a, b, c, d) => {
console.log(`message: ${a}`); // message: Uncaught ReferenceError: a is not defined
console.log(`source: ${b}`); // source: http://localhost:8000/test.html
console.log(`lineno: ${c}`); // lineno: 24
console.log(`colno: ${d}`); // colno: 1
return true;
};
a.b = 123
Promise 异常:和普通异常不同,未被 catch 的 Promise 异常需要通过 window.addEventListener('unhandledrejection', callback) 捕获。
window.addEventListener("unhandledrejection", (evt) => {
console.log(`unhandledrejection: ${evt.reason}`); // TEST
});
Promise.reject('TEST')
资源加载异常:可以通过 window.addEventListener('error', callback, true) 捕获,在回调函数中需要判断 event target 类型,过滤出 script/link/image 等资源。值得注意的是资源加载引发的错误无法被 window.onerror 抓取,原因是它们没有冒泡,只能在捕获阶段获取到。
<script>
window.addEventListener('error', (evt) => {
if (evt.target.tagName === 'IMG') {
console.log('Image load error: ', evt.target.src);
// Image load error: http://localhost:8000/not-found-image.jpg
}
}, true)
</script>
<img src="./not-found-image.jpg">
请求异常:可以通过覆写 fetch 和 XMLHttpRequest 实现,在响应里判断 HTTP 状态码或自定义的业务异常码后上报。
const originalFetch = window.fetch;
// 重写 fetch 函数
window.fetch = function(url, options) {
return new Promise((resolve, reject) => {
// 调用原始的 fetch 函数发起请求
originalFetch(url, options)
.then(response => {
// 检查响应的状态码并上报...
})
.catch(error => {
// 捕获请求异常...
});
});
};
2.2 日志上报
Image 标签:在页面中添加一个图片,将日志内容拼接在 URL 里。本质是触发 GET 请求,简单易用。 XHR/fetch:灵活的异步请求,能发送大量数据。 navigator.sendBeacon() :适用于需要在页面卸载时或导航发生时发送数据,它是异步非阻塞的,浏览器会尽量完成数据发送而不影响新页面载入。不过它不支持自定义 header 等信息,有需求可以用 fetch API 的 keepalive 属性(和 HTTP header 中的 Keep-Alive 不是一回事)实现同样的功能。 APP 代理:如果页面运行在原生 APP 中,通过 jsbridge 将日志委托给客户端上报也是一种常用手段。
public fireTasks() {
if (!(this.queues && this.queues.length)) {
return;
}
new Report('ERRORS', this.collector).sendByXhr(this.queues);
this.queues = [];
}
public finallyFireTasks() {
window.addEventListener('beforeunload', () => {
if (!this.queues.length) {
return;
}
new Report('ERRORS', this.collector).sendByBeacon(this.queues);
});
}
public traceInfo() {
// ...
Task.addTask(this.logInfo, collector);
Task.finallyFireTasks();
if (interval) {
return;
}
interval = setInterval(() => {
Task.fireTasks();
}, 60000);
}
分布式追踪
在复杂的分布式系统中,一个请求可能会经过多个不同的服务和组件进行处理,涉及多个网络调用和跨越不同的进程或服务之间的通信。分布式追踪的目标是跟踪整个请求的路径,并收集关键的性能指标,以便全面了解请求在系统中的执行情况。
简单来说,SkyWalking 通过维护 Trace Context 实现链路追踪和分析。它制定了 Cross Process Propagation Headers Protocol v3(sw8)协议用于上下文传播,这个协议是由 - 分割的 8 个字段组成的,具体含义可参考官方文档。
SkyWalking Client JS 覆写了 fetch 和 XHR,发出请求时会根据协议规范生成数据并在 header 中加入 sw8 字段。SkyWalking 检测到符合协议的数据后,就会解析它并作为上下文传递到后续服务中,由此实现了以浏览器请求为起点的链路追踪。
const traceIdStr = String(encode(traceId)); // uuid 生成的随机值
const segmentId = String(encode(traceSegmentId)); // uuid 生成的随机值
const service = String(encode(segment.service)); // 应用名称
const instance = String(encode(segment.serviceInstance)); // 应用版本
const endpoint = String(encode(customConfig.pagePath)); // 页面路径
const peer = String(encode(url.host)); // location.host
const index = segment.spans.length;
const values = `${1}-${traceIdStr}-${segmentId}-${index}-${service}-${instance}-${endpoint}-${peer}`;
headers['sw8'] = values;
前端请求成功后,可以在 SkyWalking 管理后台查看追踪数据,包含 PV、异常数量和比例、每个页面(endpoint)的性能数据等:
SkyWalking 后台展示的异常指标数据
SkyWalking 后台展示的性能指标数据
还可以追踪具体请求,可以看到前端请求和后端已经打通了,根据调用链能够清晰地定位到后端异常的发生位置,点击详情能够直接查看日志:
特定请求对应的链路
总结
参考资料
https://skywalking.apache.org/docs/main/next/en/api/x-process-propagation-headers-v3/
https://skywalking.apache.org/blog/end-user-tracing-in-a-skywalking-observed-browser/
https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon
https://developer.mozilla.org/en-US/docs/Web/API/Performance_API
https://web.dev/articles/vitals?hl=zh-cn
关于领创集团