前端性能优化--卡顿定位方案

2024-01-27 09:43:24 浏览数 (3)

接上篇《卡顿的监控方案》,我们来介绍一下监控到卡顿之后,要怎么进行定位。

卡顿埋点上报

不管是哪种卡顿监控方式,我们使用检测卡顿的方案发现了卡顿之后,需要将卡顿进行上报才能及时发现问题。但如果我们仅仅上报了卡顿的发生,是不足以定位和解决问题的。

卡顿打点

那么,我们可以通过打点的方式来大概获取卡顿发生的位置。

举个例子,假设我们一个网页中,关键的点和容易产生长耗时的操作包括:

  1. 加载数据。
  2. 计算。
  3. 渲染。
  4. 批量操作。
  5. 数据提交。

那么,我们可以在这些操作的地方进行打点。假设我们卡顿工具的能力主要有两个:

代码语言: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中的方法进行打点。如果埋点数据过少,可能会产生误报,那么我们可以增加心跳的打点:

代码语言:js复制
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 代码资源拉取回来后的时机,然后进行打点:

代码语言:js复制
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腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!

0 人点赞