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,有這樣的場景,游戲開發中,前景內容需要變化較快如每秒60幀,而背景可能速度較慢如每秒10幀,這樣便可利用人眼的一些視覺特性達到一定程度的立體感(遠遠看山水的效果),這樣會更吸引用戶的眼球。
離屏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(),沒有做個實驗對照。
請謹慎使用這一技巧,因為它很大程度上依賴於底層的canvas實現,因此很容易發生變化。
context.fillRect()//顏色填充 context.clearRect(0, 0, w, h) canvas.width = canvas.width; // 一種畫布專用的技巧
避免使用陰影
減少使用 shadowBlur 效果,陰影渲染的性能開銷通常比較高
context.shadowOffsetX = 5; context.shadowOffsetY = 5; context.shadowBlur = 4; context.shadowColor = 'rgba(255, 0, 0, 0.5)'; context.fillRect(20, 20, 150, 100);
坐標值盡量使用整數
避免使用浮點數坐標,使用非整數的坐標繪制內容,系統會自動使用抗鋸齒功能,嘗試對線條進行平滑處理,這又是一種性能消耗。

可以調用 Math.round 四舍五入取整,或者floor向上ceil向下取整,trunc直接丟棄小數位。對應的
當然性能最優越的方法莫過於將數值加0.5然后對所得結果進行移位運算以消除小數部分。
1 rounded = (0.5 + somenum) | 0; 2 rounded = ~~ (0.5 + somenum); 3 rounded = (0.5 + somenum) << 0;
避免大量計算造成阻塞
所謂「阻塞」,可以理解為不間斷運行時間超過 16ms 的 JavaScript 代碼,導致頁面卡頓,丟幀,或者失去響應,這種問題能很快被用戶察覺到,造成很差的交互體驗。

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

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