Skip to content

大屏性能优化

公司项目中的有个大屏反馈说长时间运行(挂一晚上)必定页面卡死。这两天查找原因后发现是内存泄漏导致的,在此记录下。

原因查找

大屏截图

首先运行大屏,打开 Chrome 开发工具的 Performance monitor ,然后在待办任务列表中处理几个步骤让 ws 给大屏推送最新数据,目的是实时查看下几个重要的性能指标「CPU 占用、JS 堆 heap大小、DOM 节点数量」

处理待办任务:

deal-todo-list

开始记录(没有处理待办任务时):

performance-monitor 开始记录

结束记录(处理5-10个待办后):

performance-monitor 结束记录

会明显发现 CPU 和 JS 内存占用都有显著提高,这肯定是哪儿内存泄漏了。才10分钟不到就这么夸张,可想而知挂一晚上是什么场景。

定位问题

一般内存泄漏可以通过 Chrome 开发工具的「Memory」来定位问题。我们来到「Memory」tab 来拍两次快照对比下:

开始记录memory

拍两次快照:「处理待办任务之前」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.jsPainter.js 都有涉及。那么我们核心看这两个和其他涉及到的文件就好了。

解决方案

在具体阅读 Gdraw.jsEditor 等源码后会发现,每次 ws 推送都会重新创建一次 Gdraw 实例,导致 DOM 虽然删除了但是引用依然存在,从而导致内存泄漏。所以解决方案就明朗了,首先是使用单例模式,保证 instance 始终只有一个,另外就是每次重新创建前就把之前的 canvas 对象全部销毁。核心代码如下:

Gdraw.js

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

js
removeAll() {
  super.removeAll();
}

另外为了让组件在数据变化后强制更新,还需要对 commander.vue 进行优化:

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」对比:

改造前

awesome