Canvas 動畫的效能優化實踐
前言
去年聖誕節有一個下雪的背景動畫的需求。在實現這個動畫的過程中加深了對 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 (callback) {
window.setTimeout(callback, 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 程式碼
2. 使用 requestAnimationFrame 製作動畫
上面有提到。
3. 清除畫布儘量使用 clearRect
一般情況下的效能:clearRect
> fillRect
> canvas.width=canvas.width;
4. 使用離屏繪製進行預渲染
當時用 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
的寬高儘量設定成實際使用的寬高,否則過多空白區域也會造成效能的損耗。
下圖顯示了使用離屏繪製進行預渲染技術所帶來的效能改善情況:
5. 儘量少呼叫 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
。當粒子物件達一定數量時效能差距就會顯示出來了。
6. 畫素級別操作儘量避免浮點運算
進行
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)
};
建議從 github clone 程式碼到本地執行。
後話
這篇文章雖然說是關於 canvas 動畫的效能優化。一些大佬也已經看出,其他方面的效能優化方案和這個大抵相同,無非是:
-
減少 API 的使用
-
使用快取(重點)
-
合併頻繁使用的 API
-
避免使用高耗能的 API
-
用 webWorker 來處理一些比較耗時的計算
-
……
希望通過閱讀這篇文章,可以在效能優化方面給你作一個參考。
你在看
相關文章
- Canvas效能優化Canvas優化
- gprof的效能優化實踐優化
- 原生canvas遊戲效能優化Canvas遊戲優化
- canvas效能優化總結Canvas優化
- 效能優化,實踐淺談優化
- SAP ABAP 效能優化實踐優化
- Canvas 最佳實踐(效能篇)Canvas
- 基於 PageSpeed 的效能優化實踐優化
- hadoop JOB的效能優化實踐Hadoop優化
- TiDB 效能分析&效能調優&優化實踐大全TiDB優化
- ⚠️Flutter 效能優化實踐 總結⚠️Flutter優化
- FlutterWeb效能優化探索與實踐FlutterWeb優化
- 前端效能優化原理與實踐前端優化
- loading動畫的效能優化-摒棄Canvas,擁抱CSS3-實現點選特效動畫優化CanvasCSSS3特效
- 前端感官效能的衡量和優化實踐前端優化
- Tree-Shaking效能優化實踐 - 實踐篇優化
- 讀小程式效能優優化實踐-筆記優化筆記
- Vue 專案效能優化 — 實踐指南Vue優化
- HBase最佳實踐-讀效能優化策略優化
- 小程式效能優化的幾點實踐技巧優化
- 關於效能優化的一些實踐優化
- ASP.NET Core 效能優化最佳實踐ASP.NET優化
- 一次效能提升300%的優化實踐優化
- TiDB 效能分析&效能調優&最佳化實踐大全TiDB
- 前端高效能動畫最佳實踐前端動畫
- 【效能優化實踐】優化打包策略提升頁面載入速度優化
- 記一次介面效能優化實踐總結:優化介面效能的八個建議優化
- 京東微信購物首頁效能優化實踐優化
- 2022 前端效能優化最佳實踐前端優化
- Flutter效能優化實踐之TimelineFlutter優化
- HybridDBforPostgreSQL,Greenplum寫入效能優化實踐SQL優化
- Redis大叢集擴容效能優化實踐Redis優化
- Hive常用效能優化方法實踐全面總結Hive優化
- 詳談[七牛直播雲]效能優化實踐優化
- 前端效能優化實踐 之 百度App個人主頁優化前端優化APP
- 直播推流端弱網優化策略 | 直播 SDK 效能優化實踐優化
- Android最佳效能實踐(3):高效能編碼優化Android優化
- Android效能優化,Startalk會話頁GIF記憶體優化實踐Android優化會話記憶體