前言
前段时间,在公司划水的时候,突然一个项目群里,上级领导@我,说一个项目出现了崩溃的现象。我诚惶诚恐,联想到项目崩溃让领导丢了脸,可能导致公司丢失合作伙伴,然后业绩下滑,裁员,导致失业人口增加,国民经济下滑,影响民族伟大复兴的计划进程...不行,不行,为了弘扬社会主义、实现民族复兴的伟大理想,赶紧收藏了一波划水群的表情包,然后开始了踩一踩性能优化的坑。。。
问题定位
首先想一想🤔🤔🤔,为什么会出现崩溃这种现象呢?.
-
猜测一:浏览器内存占用过大😥😥
不可能啊,我的代码怎么会有变量没及时回收的情况呢?
一刻钟之后......
可能是写代码的时候,老板的猫猫不小心走了过来,然后偶尔摸了摸猫,然后不小心忘记了写完而已,然后不小心造成了内存泄漏,某些变量没有及时回收,某些事件接触绑定,某些定时器没及时清理,浏览器内存不断增加,占用过大,这纯粹是偶然事件啊
这...得验证下吧。问题来了,怎样监测浏览器内存是否泄漏?首先各种高级前端交流群里问一波热心网友小A: 出门左转百度 , 狗头.gif ...<br> 热心网友小B: 一起盖楼,帮帮忙,追不上对手了,爱你么么哒.gif ...<br> 热心网友小C: @热心网友小B 警告! 抓住咽喉.png...<br>哎,折腾一番,终于知道了怎样用谷歌浏览器查看。首先
F12,点击memory,选择中间那项,点击start,如下图 然后会出现时间线记录,灰色表示内存被使用后释放了,蓝色表示没有被释放,就是一直都占用着(这是某个网站,不是个人项目的,个人项目的占用太多,怕被吐槽)。嗯,真香!
ps:关于更多这方面的知识,有个小姐姐翻译的文章值得看看:传送门
至于内存占用的解决方案下面再详细说说 -
猜测二: CPU占用率超过极限😭😭😭 内存占用验证了,但是服务器的配置也还行,浏览器所占用的内存也远远没达到服务器运行内存的总量,还是隔了几个小时就会有崩溃现象,脑壳疼。。。咦,一不小心到了6点,又到下班时间啦,
live to work or work to live ? It's a problem

一天之后......
第二天一上午开个吹水会,晃悠而过,又到了风和日丽的午后,在睡眼朦胧之中,不尤感叹时光如白驹过隙,渺沧海之一粟, 羡长江之无穷......忽然想起昨天的问题还没想明白,不能再让岁月蹉跎🤔🤔.
考虑到这个项目很多展示类的功能,图表用得比较多,某个页面最多20多个图表,然后每个图都有刷新动画效果,每个图刷新数据频率又不一样,有的一分钟,有的10分钟,有的一小时,所以弄了大概40个定时器,一边做定时动画,一边定时请求数据......会不会是这些动画再某些时候同时重新渲染canvas,导致超过CPU负载,导致了浏览器崩溃?
验证下试试,打开任务管理器,点击性能,看看CPU使用率,大多数时候,是平稳的波浪线,波动不大,但在canvas图表重新渲染的时候,CPU利用率会瞬间飙升,多个图表同时渲染几乎达到80%以上。。。嗯,真香!

优化实现
-
组件销毁前,及时清理定时器clearTimeout
-
组件销毁前,及时解除事件的监听removeEventListener,如果有事件监听的话
-
减少定时器的使用,多个定时器整合到一个来控制
首先这些图表,既有刷新动画,又有数据的定时刷新,刷新动画其实是canvas重新渲染一次,有数据更新时,也会重新渲染一次。
定时动画方面:我在父组件定义一个animateTimer来控制定时动画,refreshHash记录这些组件的hash值,hash值在组件内部绑定了canvas的key,然后遍历refreshHash,改变hash来改变canvas的key,触发canvas重新渲染,为了避免同时渲染,CPU负载过大,每次执行refreshAnimate,都会延时10s后,再触发下一个hash值改变//parent.vue //template <dayExportTrafficAmount :hash="refreshHash.hash1" ref="charts1"></dayExportTrafficAmount> <bayonetTraffic :hash="refreshHash.hash2" ref="charts2"></bayonetTraffic> ... //script //动画刷新定时器 animateTimer = null; //组件hash值 refreshHash = { hash1: Math.random(), hash2: Math.random() ... }; /** * 刷新动画 */ async refreshAnimate() { for (const key in this.refreshHash) { if (this.refreshHash.hasOwnProperty(key)) { await this.reAnimate(this.refreshHash, key, 10000); } } this.refreshAnimate(); } reAnimate(hashObj, key, timeOut) { return new Promise((resolve, reject) => { clearTimeout(this.animateTimer); this.animateTimer = null; this.$nextTick(() => { this.animateTimer = setTimeout(() => { hashObj[key] = Math.random(); resolve(); }, timeOut); }); }); }数据刷新方面:把多个同时刷新频率的组件,整合由一个
refreshObject来管理,记录各组件的refs值来,遍历refs,通过ref调用组件内部方法重新获取数据,进行更新,也是为了避免出现同时渲染canvas,每个组件调用初始化后,都会通过refreshDelay延时,再执行下一个组件初始化//script //刷新数据对象 refreshObject = { refs: [ "charts1", "charts2" ... ], timer: null, refreshTimer: null, timeOut: 60 * 1000 }; async refreshChart() { await this.refreshData(this.refreshObject); this.refreshChart(); } /** * @param {object} dataObject * @description 定时刷新数据 */ refreshData(dataObject) { return new Promise((resolve, reject) => { clearTimeout(dataObject.timer); dataObject.timer = null; dataObject.timer = setTimeout(async () => { for ( let index = 0; index < dataObject.refs["length"]; index++ ) { let chartComponent = this.$refs[dataObject.refs[index]]; await this.refreshDelay( chartComponent["initData"], dataObject ); chartComponent = null; } resolve(); }, dataObject.timeOut); }); } /** * @description 延时刷新数据 */ refreshDelay(func, obj) { return new Promise((resolve, reject) => { clearTimeout(obj.refreshTimer); obj.refreshTimer = null; obj.refreshTimer = setTimeout(() => { func(); resolve(); }, 5000); }); }这样优化代码实现后,定时器由原来的30个,变成几个,然后canvas渲染是一个个先后绘制,也不会再出现大量图表同时渲染的问题,稳定性提升了很多
今天这样差不多可以了,
git add .>git commit -m>git push --force, 走在回家的路上,不由得感叹,我想,王国维大概是比较懂行的吧,不论怎样找答案,最后都得百度:昨夜西风凋碧树。独上高楼,望尽天涯路
衣带渐宽终不悔,为伊消得人憔悴
众里寻他千百度,蓦然回首,那人却在,灯火阑珊处 -
canvas动画优化升级--离屏渲染
离屏渲染主要用到了OffscreenCanvas,这个是个比较新的canvas技术,用的时候得考虑下浏览器兼容(丢,有得玩,当然是先玩玩啊,'After me,the flood'!🤣🤣🤣) 首先说说
OffscreenCanvas是什么鬼吧。它的原理按照个人理解,大概是:首先由一个存在DOM上的canvas元素做容器,通过OffscreenCanvas对象来完成那些帧动画或是一些复杂运算,然后把这些运算结果显示在canvas元素上。与原来的canvas直接绘制的不同之处是,OffscreenCanvas是和DOM区分开来的,并不会直接操作canvas标签元素,就是说减少了DOM操作,理论上提升了性能。由于这种特性,使得OffscreenCanvas能worker中运行(worker中不能使用DOM)--这极其重要,后面会说到
其中一种OffscreenCanvas用法如下:
<canvas id="one"></canvas>let bitmapContext= document.getElementById("one").getContext("bitmaprenderer"); let offscreen = new OffscreenCanvas(256, 256); let ctx = offscreen.getContext('webgl') ... //这里进行一些动画绘制 ... let bitmapObj= offscreen.transferToImageBitmap(); bitmapContext.transferFromImageBitmap(bitmapObj);尽力而为解释下上述代码吧,
canvas通过getContext("bitmaprenderer")得到一个ImageBitmapRenderingContext,这个东东提供一个方法transferFromImageBitmap()来悄悄地替换显示OffscreenCanvas上下文绘制的动画内容,而这些绘制的动画内容是ImageBitmap位图图像格式的,ImageBitmap数据由OffscreenCanvas上下文的transferToImageBitmap()方法得到,这个方法的作用就是获取OffscreenCanvas上下文最后绘制的图像,转换成ImageBitmap格式。整个流程就是OffscreenCanvas保存最后绘制的图像,把图像数据以偷天换日的方式转移到canvas上
方便大家理解,贴几张MDN的图
-
复杂运算优化升级--Web Worker玩起来
Web Worker相信多数人都了解,这要从js的特性说起。众所周知js是单线程的,同时只能做一件事,为了利用多核CPU的计算能力,Web Worker允许js引入脚本创建多线程,子线程完全受主线程控制住,子线程中执行的代码不会阻塞主线程,但子线程不能进行DOM操作

Web Worker虽然不能操作DOM,但canvas提供transferControlToOffscreen方法,将canvas转为OffscreenCanvas,然后传递给worker,在worker中获取OffscreenCanvas上下文,
再进行复杂运算,示例代码:
//main.js
const htmlCanvas = document.getElementById("canvas");
const offscreen = htmlCanvas.transferControlToOffscreen();
const worker = new Worker("offscreencanvas.js");
worker.postMessage({canvas: offscreen}, [offscreen]);
//worker.js
onmessage = function(evt) {
const canvas = evt.data.canvas;
const ctx = canvas.getContext('2d');
function render(time) {
// 执行动画
requestAnimationFrame(render);
}
requestAnimationFrame(render);
};
这样做到不用在主线程直接运算绘制动画,而造成阻塞,又不会直接操作DOM。同理,还可以把定时器也放在worker里面,进一步进行优化。
- 最后一点,如果有用
echart的话,组件销毁时,记得销毁echart实例;如果是重新渲染的话,先执行clear(),再setOption()🤪
总结
好了,这里主要介绍了个人做项目遇到canvas动画过多、以及过多定时器而遇到性能问题的处理方法,主要从避免内存泄、以及动画优化入手,及时清除定时器、解绑监听,回收变量,进行离屏canvas渲染动画,以及结合web worker开辟多线程进行复杂运算。拜谢!🤣🤣🤣