作者:方勤
原文:https://blog.csdn.net/weixin_39843414/article/details/103502053

前言

去年圣诞节有一个下雪的背景动画的需求。在实现这个动画的过程中加深了对 canvas 动画的一些了解,在这里我仅是抛砖引玉的分享一下,欢迎各位大佬批评。

代码已上传至 github 【https://github.com/wanqihua/blog】,感兴趣的可以 clone 代码到本地运行。

入题

需求给出的 UI 样式如下:

UI 的需求是雪花下落的方向有点倾斜角度,每片雪花的下落速度不一样但要保持在一个范围内。

需求了解的差不多就开始实现这个效果(在看这篇文章之前你需要对 canvas 的一些基本 API 了解)。

drawImage

drawImage 可传入 9 个参数,上图中的 5 个参数是比较常用的,另外几个参数是拿来剪切图片的。

直接使用 drawImage 来剪切图片,其性能不会太好,建议先将需要使用的部分用一个离屏 canvas 保存起来,需要用到的时候直接使用即可。

requestAnimationFrame

requestAnimationFrame 相对于 setinterval 处理动画有以下几个优势:

  • 经过浏览器优化,动画更流畅
  • 窗口没激活时,动画将停止,节省计算资源
  • 更省电,尤其是对移动终端

这个 API 不需要传入动画间隔时间,这个方法会告诉浏览器以最佳的方式进行动画重绘。

由于兼容性问题,可以使用以下方法对 requestAnimationFrame 进行重写:

window.requestAnimationFrame = (function () {return window.requestAnimationFrame ||window.webkitRequestAnimationFrame ||window.mozRequestAnimationFrame ||window.oRequestAnimationFrame ||window.msRequestAnimationFrame ||function (cb) {window.setTimeout(cb, 1000 / 60)}
})()

对于其他 API 烦请查阅文档。

第一次尝试

有一个大概想法后就开心的开始写代码了,基本思路就是使用 requestAnimationFrame 来刷新 canvas 画板。

由于雪花不规则,所以雪花是 UI 提供的图片,既然是图片我们就需要先将图片预加载好,要不然在转换图片的时候很可能影响性能。

使用的预加载方法如下:

function preloadImg(srcArr){if(srcArr instanceof Array){for(let i = 0; i < srcArr.length; i++){let oImg = new Image();oImg.src = srcArr[i];}}
}

前前后后写了一个下午,算是写好了,在手机上查看的效果发现很是卡顿。100 片雪花 FPS 竟然才 40 多。而且在某些机型会出现抖动的情况。

要是产品看到这个效果,恐怕是又要召集相关人员开相关会议了。这么卡顿肯定是写了些开销大的代码,于是乎需要第二次尝试。

晚上还是需要按时下班的。不过下班回家后也不能闲着,开始找相关的资料,以便第二天快速的完成。

第二次尝试前的准备

经过一个晚上的查找学习,大概知道了以下几个优化 canvas 性能的方法:

  1. 使用多层画布绘制复杂场景

分层的目的是降低完全不必要的渲染性能开销。

即:将变化频率高、幅度大的部分和变化频率小、幅度小的部分分成两个或两个以上的 canvas 对象。也就是说生成多个 canvas 实例,把它们重叠放置,每个 Canvas 使用不同的 z-index 来定义堆叠的次序。

<canvas style="position: absolute; z-index: 0"></canvas>
<canvas style="position: absolute; z-index: 1"></canvas>
// js 代码
  1. 使用 requestAnimationFrame 制作动画

上面有提到。

  1. 清除画布尽量使用 clearRect

一般情况下的性能:clearRect > fillRect > canvas.width=canvas.width;

  1. 使用离屏绘制进行预渲染

当时用 drawImage 绘制同样的一块区域:

  • 若数据源(图片、canvas)和 canvas 画板的尺寸相仿,那么性能会比较好;
  • 若数据源只是大图上的一部分,那么性能就会比较差;因为每一次绘制还包含了裁剪工作。

第二种情况我们就可以先把待绘制的区域裁剪好,保存在一个离屏的 canvas 对象中。在绘制每一帧的时候,在将这个对象绘制到 canvas 画板中。

drawImage 方法的第一个参数不仅可以接收 Image 对象,也可以接收另一个 Canvas 对象。而且,使用 Canvas 对象绘制的开销与使用 Image 对象的开销几乎完全一致。

当每一帧需要调用的对象需要多次调用 canvasAPI 时,我们也可以使用离屏绘制进行预渲染的方式来提高性能。

即:

let cacheCanvas = document.createElement("canvas");
let cacheCtx = this.cacheCanvas.getContext("2d");
cacheCtx.save();
cacheCtx.lineWidth = 1;
for(let i = 1;i < 40; i++){cacheCtx.beginPath();cacheCtx.strokeStyle = this.color[i];cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);cacheCtx.stroke();
}
this.cacheCtx.restore();
// 在绘制每一帧的时候,绘制这个图形
context.drawImage(cacheCtx, x, y);

cacheCtx 的宽高尽量设置成实际使用的宽高,否则过多空白区域也会造成性能的损耗。

下图显示了使用离屏绘制进行预渲染技术所带来的性能改善情况:

  1. 尽量少调用 canvasAPI ,尽可能集中绘制

如下代码:

for (var i = 0; i < points.length - 1; i++) {var p1 = points[i];var p2 = points[i + 1];context.beginPath();context.moveTo(p1.x, p1.y);context.lineTo(p2.x, p2.y);context.stroke();
}

可以改成:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {var p1 = points[i];var p2 = points[i + 1];context.moveTo(p1.x, p1.y);context.lineTo(p2.x, p2.y);
}
context.stroke();

tips: 写粒子效果时,可以使用方形替代圆形,因为粒子小,所以方和圆看上去差不多。有人问为什么?很容易理解,画一个圆需要三个步骤:先 beginPath,然后用 arc 画弧,再用 fill。而画方只需要一个 fillRect。当粒子对象达一定数量时性能差距就会显示出来了。

  1. 像素级别操作尽量避免浮点运算

进行 canvas 动画绘制时,若坐标是浮点数,可能会出现 CSSSub-pixel 的问题.也就是会自动将浮点数值四舍五入转为整数,在动画的过程中就可能出现抖动的情况,同时也可能让元素的边缘出现抗锯齿失真情况。

虽然 javascript 提供了一些取整方法,像 Math.floor, Math.ceil, parseInt,但 parseInt 这个方法做了一些额外的工作(比如检测数据是不是有效的数值、先将参数转换成了字符串等),所以,直接用 parseInt 的话相对来说比较消耗性能。
可以直接用以下巧妙的方法进行取整:

function getInt(num){var rounded;rounded = (0.5 + num) | 0;return rounded;
}

另 for 循环的效率是最高的,感兴趣的可以自行实验。

第二次尝试

通过昨天晚上的查阅,对这个动画做了以下几点优化:

  • 使用离屏绘制进行预渲染
  • 减少部分 API 的使用
  • 浮点数取整
  • 缓存变量
  • 使用 for 循环,替代 forEach
  • 将整体代码使用原型链方式改写了一遍

方案写好了就开始愉快的写代码了。

200 片雪花的时候 FPS 基本稳定在 60,而且抖动的情况也没了;
增加到 1000 片的时候, FPS 还是基本稳定在 60;
增加到 1500 片的时候,稍微有点零星的卡帧;
增加到 2000 片的时候,开始卡顿。

这说明这个动画还是没有优化好,还有优化空间,请各位大佬不吝指教。

推荐使用 stats.js 插件,这个插件可以显示动画运行时的 FPS。

主要代码

let snowBox = function () {let canvasEl = document.getElementById("snowFall");let ctx = canvasEl.getContext( 2d );canvasEl.width = window.innerWidth;canvasEl.height = window.innerHeight;let lineList = []; // 雪的容器let snow = function () {let _this = this;_this.cacheCanvas = document.createElement("canvas");_this.cacheCtx = _this.cacheCanvas.getContext("2d");_this.cacheCanvas.width = 10;_this.cacheCanvas.height = 10;_this.speed = [1, 1.5, 2][Math.floor(Math.random()*3)];                // 雪花下落的三种速度,便于取整_this.posx = Math.round(Math.random() * canvasEl.width);               // 雪花x坐标_this.posy = Math.round(Math.random() * canvasEl.height);              // 雪花y坐标_this.img = `./img/snow_(${Math.ceil(Math.random() * 9)}).png`;        // img_this.w = _this.getInt(5 + Math.random() * 6);_this.h = _this.getInt(5 + Math.random() * 6);_this.cacheSnow();};snow.prototype = {cacheSnow: function () {let _this = this;// _this.cacheCtx.save();let img = new Image();   // 创建img元素img.src = _this.img;_this.cacheCtx.drawImage(img, 0, 0, _this.w, _this.h);// _this.cacheCtx.restore();},fall: function () {let _this = this;if (_this.posy > canvasEl.height + 5) {_this.posy = _this.getInt(0 - _this.h);_this.posx = _this.getInt(canvasEl.width * Math.random());}if (_this.posx > canvasEl.width + 5) {_this.posx = _this.getInt(0 - _this.w);_this.posy = _this.getInt(canvasEl.height * Math.random());}// 如果雪花在可视区域if (_this.posy <= canvasEl.height || _this.posx <= canvasEl.width) {_this.posy = _this.posy + _this.speed;_this.posx = _this.posx + _this.speed * .5;}_this.paint();},paint: function () {ctx.drawImage(this.cacheCanvas, this.posx, this.posy)},getInt: function(num){let rounded;rounded = (0.5 + num) | 0;return rounded;}};let control;control = {start: function (num) {for (let i = 0; i < num; i++) {let s = new snow();lineList.push(s);}(function loop() {ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);for (let i = 0; i < num; i++) {lineList[i].fall();}requestAnimationFrame(loop)})();}};return control;
}();
window.onload = function(){snowBox.start(2000)
}

后话

这篇文章虽然说是关于 canvas 动画的性能优化。一些大佬也已经看出,其他方面的性能优化方案和这个大抵相同,无非是:

  • 减少 API 的使用
  • 使用缓存(重点)
  • 合并频繁使用的 API
  • 避免使用高耗能的 API
  • 用 webWorker 来处理一些比较耗时的计算
  • ……

希望通过阅读这篇文章,可以在性能优化方面给你作一个参考。

Canvas 动画的性能优化实践相关推荐

  1. 手机端html5 面试,今日头条 张祖俭 - H5动画在移动平台上的性能优化实践

    1.H5动画在移动平台上 的性能优化实践 今日头条 张祖俭 2.大纲 Part 1. H5动画 在移动平台上的性能问题 Part 2. 解决思路-从浏览器渲染入手 Part 3. 在H5Animato ...

  2. 【前端性能优化实践】手把手教你实现webpack图片压缩插件

    前言 我想写一个系列:前端性能优化实践方案.网上虽然一搜一大把这样的文章,但大多缺乏体系化.也有很多讲性能优化的书籍,但其实想照着书上的知识进行实践,还是挺难的一件事. 这是该系列的第一篇文章 由于自 ...

  3. 赠书:《Java性能优化实践》,众多业内大佬推荐阅读

    没有捷径可走的 Java 性能优化 多年来,用 Google 搜索 Java performance tuning,出现的三篇最热门文章之一是于 1997 年到 1998 年左右发表的文章,这篇文章在 ...

  4. Hadoop YARN:调度性能优化实践【转】

    原文地址:https://www.infoq.cn/article/dh5UpM_fJrtj1IgxQDsq 背景 YARN 作为 Hadoop 的资源管理系统,负责 Hadoop 集群上计算资源的管 ...

  5. 让Elasticsearch飞起来!——性能优化实践干货

    让Elasticsearch飞起来!--性能优化实践干货 2018年12月19日 23:01:39 铭毅天下(公众号同名) 阅读数:8805更多 所属专栏: 深入详解Elasticsearch 版权声 ...

  6. 从工具到社区,美图秀秀大规模性能优化实践

    导读:本文由演讲整理而成.美图秀秀社区自上线以来已经有近一年时间,不管是秀秀海量的用户还是图片社区特有的形态都给性能优化提出了巨大的挑战.本文将会结合这一年内我们遇到的具体案例和大家分享下美图秀秀社区 ...

  7. 前端性能优化实践 | 百度APP个人主页优化

    性能是每个前端工程师都应该关注的话题,通用的优化手段已有许多文章和实践,就不再赘述,本篇以百度 App 个人主页为例,聊聊针对业务特点进行的一些性能优化实践.适用于:传统意义的优化手段能用的都用了:打 ...

  8. Hadoop YARN:调度性能优化实践

    背景 YARN作为Hadoop的资源管理系统,负责Hadoop集群上计算资源的管理和作业调度. 美团的YARN以社区2.7.1版本为基础构建分支.目前在YARN上支撑离线业务.实时业务以及机器学习业务 ...

  9. html个人主页_前端性能优化实践 之 百度App个人主页优化

    作者:潘铭 @祝余 前言 性能是每个前端工程师都应该关注的话题,通用的优化手段已有许多文章和实践,就不再赘述,本篇以百度App个人主页为例,聊聊针对业务特点进行的一些性能优化实践. 适用于:传统意义的 ...

最新文章

  1. unity, access standard shared emission by script
  2. 用完成端口开发大响应规模的Winsock应用程序
  3. jQuery图片上传前先在本地预览(不经过后端处理)
  4. OpenCV 距离变换的笔记
  5. BootStrap之前奏响应式布局
  6. 在Windows 7 Media Center中创建音乐播放列表
  7. 循环队列及C语言实现二
  8. cocos creator 方法数组_基于 Cocos 游戏引擎的音视频研发探索
  9. 【飞秋】JS 实现完美include
  10. 计算机被格式化怎么找回资料,电脑文档被格式化,怎么恢复格式化文档
  11. import java.io后报错_【JAVA小白】 问关于做IO流作业的时候出错了,错误FileOutputStream.writeBytes...
  12. MySql的完整卸载(总共四个步骤)
  13. 《高等代数学》(姚慕生),习题1.1:二阶行列式
  14. win10系统下摄像头无法打开的解决方法
  15. Pt100铂电阻与惠斯通电桥
  16. 《哈利波特》购书最低折扣
  17. 计算机下桌面显示不出来,电脑桌面文档不会在右边显示出来怎么办
  18. 8xmax升级鸿蒙,配置设计各种寒酸:Redmi10X开箱
  19. 计算机辅助培训的策略,宁波诺丁汉大学学习策略培训对解决计算机辅助语言教学环境下信息过剩问题的启示...
  20. MATLAB resample函数

热门文章

  1. LVDS接口分类与数据格式
  2. vue 获取视频第一帧
  3. Java 使用javaCV获取 视频的某一帧是第几秒 视频的第几秒是第几帧
  4. 欧姆龙PLC的CIP协议报文
  5. 【逗老师的无线电】MMDVM串口屏相关开发
  6. 摄像头安全隐患大安防层级待提升
  7. OPERA更改房间类型(维护里)
  8. jcrop 用法小结
  9. google翻译逆天了逆天了(搞笑版)
  10. 苹果录屏怎么设置_电脑怎么设置录屏?怎么设置电脑自动录屏?