PDA 使用 uniapp 扫码几百单后页面卡顿的优化小结

4,014 阅读5分钟

由于本项目还未正式上架应用商店,下文中将不会展示一些可能导致泄密的信息,只是对优化过程的一次总结

公司做的是物流相关的业务,最近用 uniapp 做的一个给 pda 使用的应用进入了测试环节,使用方反馈了一个性能问题:用 pda 扫描包裹条形码进行入库操作,开始时录入每一条记录的响应时间都很短,速度很快,但是当扫了200单左右时,录入单个包裹的时间变长,会有比较明显的卡顿感。

收到反馈后,身为前端的我开始着手解决问题。那么,优化 app 性能第一步,得搞清楚问题出在了哪?

首先想到的就是内存泄漏或是内存溢出

以下内容摘自百度百科

  • 内存泄漏(Memory Leak) 是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
  • 内存溢出(Out Of Memory,简称OOM) 是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存

提出了假设就可以开始寻找证据了。因为是 uniapp 做的 app,得利于其拥有的跨端能力,我将出问题的页面以网页的形式通过浏览器打开,利用 Chrome 的 DevTools 工具进行测试。

1. 按下 Shift + Esc 键调出任务管理器窗口,观察 JavaScript 使用的内存情况

js内存.gif

发现页面在没有进行任何操作的时候,js 使用的内存已经在缓慢上升了。。。

v2-c71b0dce2effd10f0252860887ec8a43_720w.jpg
显然并不合理,这下不能直接甩锅给后端了,赶紧偷偷把导致内存不断上升的代码改了再甩锅。于是再次看了遍自己写的 js,发现有个定时器

this.timer = setInterval(function() {
    uni.hideKeyboard()
}, 60)

当初这样写是因为 pda 的扫码机制就是在模拟键盘输入,所以我在这页面弄了个自动获得焦点的 input,然后把它隐藏了起来。但是这样会导致进入页面后软键盘自动弹出,操作起来不是很友好。所以我搞了个定时器让它不断的隐藏软键盘,然后在离开页面的时候清除这个定时器。现在看来这样偷懒是行不通的,时间久了内存可不就增加了,积少成多,终究影响页面性能。于是更改了清除定时器的条件

let index = 0
if (!this.timer) {
    this.timer = setInterval(() => {
        if(index === 5) {
            clearInterval(this.timer)
            this.timer = null
        }
        uni.hideKeyboard()
        index++
    }, 200)
}

进入页面后执行完 5 次收起软键盘的操作就清除掉定时器。(不直接 uni.hideKeyboard()是因为这样会导致其它需要调用软键盘的页面无法调起软键盘)。再次通过 Chrome 测试,发现在没有任何操作的情况下,js 使用的内存不再增加,收工!

然鹅故事并未结束,重新打包了一份安装到 pda 上信心满满的开始扫码测试。这里插上一句,之所以特意写篇小作文记录这次优化,是因为自动测试不能复现卡顿,每次测试都要手动扫个 200 多单,心里苦啊~~~

049752cd7b899e5155b7965f4ca7d933c8950d29.jpg

扫码测试的结果是,卡顿依旧出现了,既意外又不意外,第一次收工失败。

2. 通过 Chrome 的 Performance 面板配合 Memory 面板检测页面性能

在浏览器页面将原本隐藏的 input 调为可见,通过手动输入单号测试,发现 JS Heap 曲线如下图

2021-04-21_140945.png

再来到 Memory 面板,选择 Allocation instrumentation on timeline 进行记录,得到的结果如下图

2021-04-21_141404.png

初步判断垃圾对象基本都被 js 引擎垃圾回收器回收了,应该不存在没发现的不合理闭包。但是卡顿依旧在,夕阳几度红?

3. 再次测试,仔细观察卡顿具体环节

这一次,我在整个流程,从按下扫码键开始到后端数据返回最终呈现在页面的各个节点都进行了 console.log,再次开始漫长且痛苦的扫描测试。。。在扫了 130 单左右时,发现从扫码到发送请求到接收请求的速度都没有异常,但是接收到请求后,页面的重新渲染与刚开始扫码时相比有所延迟。

Thank God,这下终于把问题弄清楚了。因为业务需求是将扫码结果返回到页面上,以列表(list)形式呈现,每扫一单(item)就将最新的结果呈现在列表的开头。这就导致了页面的“重绘”与“重排”(亦称为“回流”)。uniapp 使用的是 Vue.js,我给每个 item 已经绑定了一个 key,用来减少不必要的性能消耗。但是因为每次数据返回后都会进行数组的 unshift 操作,所以重排不可避免的发生了,重排又带来了重绘,数据少时没什么明显感知,数据多了后开销就大了,于是在一些老旧的机子上就会出现卡顿。

4. 优化方案

最终的优化方案除了之前提到的提前销毁收起软键盘的定时器,主要是将原本直接全盘呈现的数据列表改为只显示最新的 20 条数据,也就是增加一步 splice(20) 的处理。另外还有些细节改动,比如在多次用到 res.data 的地方提前声明一个变量将值存储起来 const resultData = res.data,以空间换时间。

One More Thing

还有个问题,在一些情况下,原本设定的请求完成就让 input 框清空的操作 this.input = '' 会无法实时在页面渲染上得到呈现,即扫码后 input 的 value 已经是空字符串了,但是输入框上还是保留着上一次扫码数据,导致下次扫码失败,使用 $nextTick() 依旧无效。后来我想到可能是 vue 的 diff 机制原因,所以给 input 绑定了个 key 属性,赋值为 inputId,在特殊条件下让 key 改变,再进行数据的清空,问题得到解决。

this.inputId = Date.now()
this.input = ''

010f985e840f22a801216518ebf3d5.gif 点赞.png