接上篇《卡顿的监控方案》,我们来介绍一下监控到卡顿之后,要怎么进行定位。
卡顿埋点上报
不管是哪种卡顿监控方式,我们使用检测卡顿的方案发现了卡顿之后,需要将卡顿进行上报才能及时发现问题。但如果我们仅仅上报了卡顿的发生,是不足以定位和解决问题的。
卡顿打点
那么,我们可以通过打点的方式来大概获取卡顿发生的位置。
举个例子,假设我们一个网页中,关键的点和容易产生长耗时的操作包括:
- 加载数据。
- 计算。
- 渲染。
- 批量操作。
- 数据提交。
那么,我们可以在这些操作的地方进行打点。假设我们卡顿工具的能力主要有两个:
代码语言:js复制interface IJank {
_jankLogs: Array<IJankLogInfo & { logTime: number }>;
// 打点
log(jankLogInfo: IJankLogInfo): void;
// 心跳
_heartbeat(): void;
};
那么,当我们在页面加载的时候分别进行打点,我们的堆栈可能是这样的:
代码语言:js复制_jankLogs = [{
module: '数据层',
action: '加载数据',
logTime: xxxxx,
}, {
module: '渲染层',
action: '计算',
logTime: xxxxx,
}, {
module: '渲染层',
action: '渲染',
logTime: xxxxx,
}, {
module: '数据层',
action: '批量操作计算',
logTime: xxxxx,
}, {
module: '数据层',
action: '数据提交',
logTime: xxxxx,
}]
当卡顿心跳发现卡顿产生时,我们可以拿到堆栈的数据,比如当用户在批量操作之后发生卡顿,假设此时我们拿到堆栈:
代码语言:js复制_jankLogs = [{
module: '数据层',
action: '加载数据',
logTime: xxxxx,
}, {
module: '渲染层',
action: '计算',
logTime: xxxxx,
}, {
module: '渲染层',
action: '渲染',
logTime: xxxxx,
}, {
module: '数据层',
action: '批量操作计算',
logTime: xxxxx,
}]
这意味着卡顿发生时,最后一次操作是数据层--批量操作计算
,则我们可以认为是该操作产生了卡顿。
我们可以将module
/action
以及具体的卡顿耗时一起上报,这样就方便我们监控用户的大盘卡顿数据了,也较容易地定位到具体卡顿产生的位置。
心跳打点
当然,上述方案如果能达到最优效果,则我们需要在代码中关键的位置进行打点,常见的比如数据加载、计算、事件触发、JavaScript 加载等。
我们可以将打点方法做成装饰器,自动给class
中的方法进行打点。如果埋点数据过少,可能会产生误报,那么我们可以增加心跳的打点:
IJank._heartbeat = () => {
IJank.log({
module: 'Jank',
action: 'heartbeat',
logTime: xxxxx,
})
}
当我们心跳产生的时候,会更新堆栈数据。假设发生卡顿的时候,我们拿到这样的堆栈信息:
代码语言:js复制_jankLogs = [{
module: '数据层',
action: '加载数据',
logTime: xxxxx,
}, {
module: 'Jank',
action: 'heartbeat',
logTime: xxxxx,
}, {
module: 'Jank',
action: 'heartbeat',
logTime: xxxxx,
}, {
module: '渲染层',
action: '计算',
logTime: xxxxx,
}, {
module: 'Jank',
action: 'heartbeat',
logTime: xxxxx,
}, {
module: '渲染层',
action: '渲染',
logTime: xxxxx,
}, {
module: 'Jank',
action: 'heartbeat',
logTime: xxxxx,
}, {
module: '数据层',
action: '批量操作计算',
logTime: xxxxx,
}, {
module: 'Jank',
action: 'heartbeat',
logTime: xxxxx,
}]
显然,卡顿发生时最后一次打点为Jank--heartbeat
,这意味着卡顿并不是产生于数据层---批量操作计算
,而是产生于该逻辑后的一个不知名逻辑。在这种情况下,我们可能还需要再在可疑的地方增加打点,再继续观察。
JavaScript 加载打点
有一个用于监控一些懒加载的 JavaScript 代码的小技巧,我们可以使用PerformanceObserver
获取到 JavaScript 代码资源拉取回来后的时机,然后进行打点:
performanceObserver = new PerformanceObserver((resource) => {
const entries = resource.getEntries();
entries.forEach((entry: PerformanceResourceTiming) => {
// 获取 JavaScript 资源
if (entry.initiatorType !== 'script') return;
// 打点
this.log({
moduleValue: 'compileScript',
actionValue: entry.name,
});
});
});
// 监测 resource 资源
performanceObserver.observe({entryTypes: ['resource']});
当卡顿产生时,堆栈的最后一个日志如果为compileScript--bundle_xxxx
之类的,则可以认为该 JavaScript 资源在加载的时候耗时较久,导致卡顿的产生。
通过这样的方式,我们可以有效监控用户卡顿的发生,以及卡顿产生较多的逻辑,然后进行相应的问题定位和优化。
结束语
对于计算逻辑较多、页面逻辑复杂的项目来说,卡顿常常是一个较大痛点。
关于日常性能的数据监控和优化方案之前也有介绍不少,相比一般的性能优化,卡顿往往产生于不合理的逻辑中,比如死循环、过大数据的反复遍历等等,其监控和定位方式也与普通的性能优化不大一致。
查看Github有更多内容噢: https://github.com/godbasin
我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!