canvas性能優化總結
canvas的主要功能就是用來繪製內容,有時候為了給用戶流暢的視覺感受,需要繪製的頻率要求很高,這樣對繪製的性能就有要求,那麼怎麼才能寫出高性能的繪製程式碼呢。
儘可能少調用api
例如我們繪製一段線條,如果用如下程式碼的話,每移動一次就stroke一次:
1 for (var i = 0; i < points.length - 1; i++) { 2 var p1 = points[i]; 3 var p2 = points[i + 1]; 4 context.beginPath(); 5 context.moveTo(p1.x, p1.y); 6 context.lineTo(p2.x, p2.y); 7 context.stroke(); 8 }
優化後程式碼如下,這樣beginPah和stroke就少調用了n次。
1 context.beginPath(); 2 for (var i = 0; i < points.length - 1; i++) { 3 var p1 = points[i]; 4 var p2 = points[i + 1]; 5 context.moveTo(p1.x, p1.y); 6 context.lineTo(p2.x, p2.y); 7 } 8 context.stroke();
盡量少改變CANVAS狀態機
我們可以改變 context 的若干狀態,而幾乎所有的渲染操作,最終的效果與 context 本身的狀態有關係。例如當對context.lineWidth賦值的話,開銷遠遠大於對一個普通對象賦值的開銷。
Canvas 上下文不是一個普通的對象,當調用了 context.lineWidth = 5 時,瀏覽器會需要立刻地做渲染上下文環境的工作,這樣你下次調用諸如 stroke 或 strokeRect 等 API 時,畫出來的線就正好是 5 個像素寬了。其實這也是瀏覽器自身的一種優化,否則如果等到stroke調用時再臨時準備渲染環境,會更加影響正常繪製情況下的性能。
下面對比優化前後的程式碼:
for (var i = 0; i < STRIPES; i++) { context.fillStyle = (i % 2 ? COLOR1 : COLOR2); context.fillRect(i * GAP, 0, GAP, 480); }
context.fillStyle = COLOR1; for (var i = 0; i < STRIPES / 2; i++) { context.fillRect((i * 2) * GAP, 0, GAP, 480); } context.fillStyle = COLOR2; for (var i = 0; i < STRIPES / 2; i++) { context.fillRect((i * 2 + 1) * GAP, 0, GAP, 480); }
上面兩段程式碼,對fillStyle的調用時機做了改變,提高了性能。
分層canvas
繪製場景複雜的情況下,一般採用多個canvas,可依據繪製內容的頻率高低來劃分。
如遊戲中的背景繪製頻率低可以放在一層canvas上,上面的小人等繪製頻率高放在一層canvas上,兩層canvas的疊加效果達到完整效果。
如下圖中繪製過程中的圓形在一層canvas上,不斷清除不斷繪製,而下面的已經繪製出來的筆跡內容放在另外一層canvas上,不需要清除重繪。


離屏canvas
也叫作預渲染,在離屏canvas上繪製好一整塊圖形,繪製好後在放到視圖canvas中,適合每一幀畫圖運算複雜的圖形。
比如我們有時候為了儘可能少的請求網路資源,會用到精靈圖,這樣在繪製精靈圖某一塊內容時,需要利用繪圖api的裁剪。
實際發現,使用 drawImage 繪製一張大尺寸圖片到較小畫布區域上,比起繪製一張和繪製區域尺寸一樣大的圖片的情形,開銷要大一些。可以認為,兩者相差的開銷正是「裁剪」這一個操作的開銷。下面三種繪製方式,性能開銷依次增加。
// 將image放到目標canvas指定位置,大小按照原圖大小渲染 void ctx.drawImage(image, dx, dy); // 將image放到目標canvas指定位置,指定寬高渲染 void ctx.drawImage(image, dx, dy, dWidth, dHeight); // 將image裁剪之後放到目標canvas指定位置,指定寬高渲染 void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
而離屏渲染就可以讓我們先把圖片裁剪成想要的尺寸內容保存起來,等到真正繪製的時候就可以使用第一種寫法簡單的把圖片繪製出來。
// 在離屏 canvas 上繪製 var offscreencanvas = document.createElement('canvas'); // 寬高賦值為想要的圖片尺寸 offscreencanvas.width = dWidth; offscreencanvas.height = dHeight; // 裁剪 offscreencanvas.getContext('2d').drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); // 在視圖canvas中繪製 viewcontext.drawImage(canvas, x, y);
有時候,遊戲對象是多次調用 drawImage 繪製而成,或者根本不是圖片,而是使用路徑繪製出的矢量形狀,那麼離屏繪製還能幫你把這些操作簡化為一次 drawImage 調用。
組合圖形組合了多個圖形將它們繪製存放到離屏canvas中,下次未變化的時候直接繪製一次離屏canvas。
裁剪
Canvas (大小一般小於等於螢幕寬高)只是整個大場景下的一個「可視窗口」,如果我們在每一幀中,都把全部內容畫出來,勢必就會有很多東西畫到 Canvas 外面去了,同樣調用了繪製 API,但是並沒有任何效果。
那麼視口外的內容是不需要繪製的,但如果繪製對性能影響有多少呢?進行這樣一個實驗,繪製一張 320x180 的圖片 104 次,當每次都繪製在 Canvas 內部時,消耗了 40ms,而每次都繪製在 Canvas 外時,僅消耗了 8ms。雖然繪製在canvas外時,消耗的時間較短。
但考慮到計算的開銷與繪製的開銷相差 2~3 個數量級,所以一般情況下通過計算來過濾掉哪些畫布外的對象,仍然是很有必要的。
局部重繪
由於 Canvas 的繪製方式是畫筆式的,在 Canvas 上繪圖時每調用一次 API 就會在畫布上進行繪製,一旦繪製就成為畫布的一部分。繪製圖形時並沒有對象保存下來,一旦圖形需要更新,需要清除整個畫布重新繪製。
如下圖僅對紅邊框的平行四邊形做改變,如果每次重繪整個畫布內容就不太合適

Canvas 局部刷新的方案:
- 清除指定區域的顏色,並設置 clip
- 所有同這個區域相交的圖形重新繪製
要實現局部渲染時,需要考慮的兩個因素是:
- 單次刷新時影響的範圍最小
- 刷新的圖形不會影響其他圖形的正確繪製
清除畫布內容
有地方說這三種方法性能依次提高,我目前只是使用了clearRect(),沒有做個實驗對照。
context.fillRect()//顏色填充 context.clearRect(0, 0, w, h) canvas.width = canvas.width; // 一種畫布專用的技巧
坐標值盡量使用整數
避免使用浮點數坐標,使用非整數的坐標繪製內容,系統會自動使用抗鋸齒功能,嘗試對線條進行平滑處理,這又是一種性能消耗。
可以調用 Math.round 四捨五入取整。
避免大量計算造成阻塞
所謂「阻塞」,可以理解為不間斷運行時間超過 16ms 的 JavaScript 程式碼,導致頁面卡頓,丟幀,或者失去響應,這種問題能很快被用戶察覺到,造成很差的交互體驗。

所以我們要把與渲染無關的大量計算交給worker。大量計算可能造成渲染不流暢,但絕對不能讓用戶操作卡頓失去響應。
像下圖的效果,需要計算大量函數曲線上的點來繪製成曲線,我們移動的時候可以看到計算新點坐標值的過程是有延遲的,但是並不會讓用戶滑鼠拖拽卡頓失效,渲染的過程再跟隨滑鼠移動。

總結
以上便是總結到的提升繪製效率的幾點建議!具體採用哪種需要在實際項目裡面根據情況來定,如果你知道這幾種方式至少不會大腦空白了!
還有幾點開發過程需要注意的:
- 儘可能使用計算代替canvas渲染
- 減少改變 context 的狀態,如果要改變請賦值正確的類型,減少瀏覽器的嘗試
- 減少使用 shadowBlur 效果,陰影渲染的性能開銷通常比較高

