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就能解决你的棘手问题;

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

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax