Debounce防抖引发的性能问题,优化后提升5倍

问题起因

页面使用webgl加载100万+数据,当数据从0、1000、10000到100万逐渐增加,鼠标点击页面选择元素的速度越来越慢。表象原因:数据量增加,每帧渲染时间增加,导致界面卡顿。但通过性能分析发现,本质原因却是和debounce相关!

老大第一次问原因,我不假思索地脱口而出:数据量太大,导致渲染帧频下降,不信你看:JS内存飙升至700MB,帧频降至13fps。

有没有解决方案?我一时也想不出办法来,主观感受是数据量太大,webgl渲染延迟。我们使用的是mapboxgl,可以说它是地图行业的前端可视化标杆(star 10K+),记得曾经有位大佬说过:"当你在质疑一个通用框架时,先想想自己的逻辑是否有问题。"

Performance分析

Chrome的Performance工具可以分析页面帧渲染耗时,以及每一个帧的调用堆栈。打开页面后,首屏渲染没发现异常情况, 每一帧耗时平均16.5ms。

当数据加载量增加至100万+,再次查看性能统计,满屏的大血条, 一帧平均耗时780ms, 相比首屏耗时直接增加了50倍。

鼠标移动到标红的Task上,显示"Long Task",什么是"Long Task"?

任何超过50ms的任务都属于耗时比较长的任务,总耗时减去50ms称为任务阻塞期。 下图中"self 0.80ms"表示函数调用时自身仅��时0.8ms,剩余的713ms可能由于其他任务抢占CPU资源导致阻塞。

什么情况会涉及到CPU资源抢占?界面点击事件click、promise、setTimeout、requestAnimationFrame、requestIdleCallback等,都可能导致资源的抢占。

Long Task排查

看着满屏的Long Task,但其实就两种:Time fired、Animiation Frame Fired。Time fired由setTimeout触发,而Animation Frame Fired则由requestAnimationFrame触发。

Time fired

为了避免用户频繁点击界面元素,给click事件添加了debounce防抖,保证在80ms内只触发最后一次。

typescript 复制代码
const clickListener = debounce(
  (cb: (...args: any[]) => void, mode: Mode, ...args: any[]) => {
    cb(mode, ...args);
  },
  80,
);

click事件负责高亮界面上的元素,当数据量达到100万+的现象是,选择效果延迟特别高。查看debounce源码:

scss 复制代码
 function startTimer(pendingFunc, milliseconds) {
    if (useRAF) {
        root.cancelAnimationFrame(timerId);
        return root.requestAnimationFrame(pendingFunc);
    }
    // eslint-disable-next-line @typescript-eslint/no-implied-eval
    return setTimeout(pendingFunc, milliseconds);
}

当debounce设置了milliseconds时,使用setTimeout延迟执行。

根据上图分析,一个Time fired类型的Task执行过程大致有:setTimeout + 逻辑代码 + microTask + 逻辑代码 + microTask。

其中microTask为Vue渲染机制Promise触发。promise优先级比较高,如果有其他Task抢占CPU资源,那问题就可能出在setTimeout。

刚才提到Long Task还有另一种���Animation Frame Fired,看名字就知道是由requestAnimationFrame触发的,webgl渲染时会使用它。requestAnimationFrame会在页面渲染前触发,一般情况下优先级requestAnimationFrame > setTimeout。

由于在界面点击时,mapboxgl底层会触发渲染(requestAnimationFrame),那有可能它就是抢占setTimeout资源的"罪魁祸首"。

尝试:先把click的debounce去掉,至于重复触发问题,可通过其他方法解决。

看看取消debounce的效果,果然大血条少了很多。当数据量达到100万+,页面交互也比之前流畅很多,没有明显的卡顿现象。

改善挺明显,也可以给老大交差了。但本着"打破砂锅问到底"的职业态度,还得继续分析另一种Long Task: Animation Frame Fired。

Animation Frame Fired

上图为一个Animation Frame Fired长任务的明细,我都惊奇了,差不多一帧里renderLayer被调用了400次。结合自身的专业嗅觉,我严重怀疑400次的renderLayer调用有问题,假如一个renderLayer耗时2ms,那总计不也得0.8s了?

题外话:为什么有400次的renderLayer调用?由于mapboxgl不支持增量更新,所有我设计框架时支持可配置创建n个Source,而每一个Source下对应20个Layer。这样设计的目的是将100万+数据均摊到n个Source,那每个Source数据做全量更新时压力就会小很多

思考:能否减少renderLayer的调用次数?

说来也是机缘巧合,一个月前,mapboxgl发布的3.0.4版本增加了增量更新的API,之前我还给官方提了Issue。

当时我把这个这个好消息告诉老大,然后升级版本, 替换增量更新API,体验非常丝滑。

有这个前提,我现在可以考虑减少layer的数量,把Source从原来的20减少至5个,那么layer的数量减少了15 * 20 = 300个。

改完之后,同样将数据量增加至100万,再看看效果,没看到大血条,搜索renderLayer关键词,也大幅减少,心里舒坦极了!

总结

优化后的效果,100万+数据,交互流畅度能够媲美首屏。

通过这次性能分析,有些颠覆我对Javascript事件队列机制的认知。当面对大数据渲染场景,如果渲染出现Long Task,那防抖、截流、Vue渲染时机可能不会按预期执行,从而为性能问题埋下"定时炸弹"。

分析总结:

  1. 性能分析得靠数据说话,即便你有比较丰富的性能优化经验,主观结论会给人不可靠感觉;
  2. 大数据场景,使用防抖、截流、requestAnimationFrame、requestIdelCallback等会抢占CPU资源函数时,一定得持"如履薄冰、谨小慎微"的态度。
  3. 你在怀疑拥有上百万周下载量的三方库性能问题时,先确认自身代码逻辑是否有问题, 结果80%概率都是自身逻辑问题;
  4. 作为三方框架的深度用户,你的痛点同时也是维护者的痛点。持续关注官方的最新版本,有时候一个新的API就能解决你的棘手问题;

我是前端下饭菜,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!

相关推荐
四喜花露水14 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy23 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生2 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web
Missmiaomiao3 小时前
npm install慢
前端·npm·node.js