大屏性能优化
公司项目中的有个大屏反馈说长时间运行(挂一晚上)必定页面卡死。这两天查找原因后发现是内存泄漏导致的,在此记录下。
原因查找
首先运行大屏,打开 Chrome 开发工具的 Performance monitor ,然后在待办任务列表中处理几个步骤让 ws 给大屏推送最新数据,目的是实时查看下几个重要的性能指标「CPU 占用、JS 堆 heap大小、DOM 节点数量」
处理待办任务:
开始记录(没有处理待办任务时):
结束记录(处理5-10个待办后):
会明显发现 CPU 和 JS 内存占用都有显著提高,这肯定是哪儿内存泄漏了。才10分钟不到就这么夸张,可想而知挂一晚上是什么场景。
定位问题
一般内存泄漏可以通过 Chrome 开发工具的「Memory」来定位问题。我们来到「Memory」tab 来拍两次快照对比下:
拍两次快照:「处理待办任务之前」VS「处理待办任务之后」
Comparison 列表中主要看 New、Deleted、Delta 三项,New 代表此次快照新增的内存占用数量,Deleted 代表此次快照销毁的内存占用数量,Delta 代表用来对比的快照增加或减少的内存占用数量,也就是 New 数量减去 Deleted 数量的结果。
在快照列表上方的 Class filter 输入框中输入 Detached ,就会筛选出游离对象的列表。
游离列表中的 Detached HTMLDivElement
代表游离的 DOM 元素,点击左侧小箭头,会出现具体的游离 DOM 信息,信息中有一项_prevClass
,代表 DOM 对应的 class ,如果这个 class 名字比较唯一,则能很容易通过这个 class 查找到相对应的代码组件,然后根据组件的代码逻辑,判断是否有涉及到 DOM 的变量引用,在 beforeDestroy 生命周期中手动销毁这个变量(将变量设置为null),往往可以解决游离 DOM 的问题。
然而我们核心区域是用 Canvas 绘制的,所以看上图的 Detached CanvasRenderingContext2D
实例就行了。然后鼠标点击详情,可在下方定位到没有释放变量引用的具体代码位置。上图可以看出 Gdraw.js
、Painter.js
都有涉及。那么我们核心看这两个和其他涉及到的文件就好了。
解决方案
在具体阅读 Gdraw.js
和 Editor
等源码后会发现,每次 ws 推送都会重新创建一次 Gdraw
实例,导致 DOM 虽然删除了但是引用依然存在,从而导致内存泄漏。所以解决方案就明朗了,首先是使用单例模式,保证 instance 始终只有一个,另外就是每次重新创建前就把之前的 canvas 对象全部销毁。核心代码如下:
Gdraw.js
constructor(dom, opts) {
const ins = this.__initZr(dom, opts);
if (ins) return ins;
...
}
...
__initZr(dom, opts) {
var id = guid();
const ins = Object.values(instances).find(v => v.dom === dom);
if (ins) return ins;
...
}
...
clear() {
this.storage.delRoot();
this.painter.clear();
}
dispose() {
...
this.clear();
this.storage.dispose();
this.painter..dispose();
this.handler.dispose();
...
this.constructor.delInstance(this.id);
}
Editor.js
removeAll() {
super.removeAll();
}
另外为了让组件在数据变化后强制更新,还需要对 commander.vue
进行优化:
<template>
<canvasView
:computedKey="computedKey"
...
></canvasView>
</template>
<script>
export default {
data() {
return {
...
computedKey: -1
}
},
watch: {
sceneData(val) {
if (val) {
...
this.computedKey = Math.random() + '';
}
}
}
}
</script>
性能对比
改造完毕后我们再来看看优化前后的「Performance monitor」对比: