簡介
HTML5 canvas 最初起源於蘋果(Apple)的一項實驗,現在已經成為了web中受到廣泛支持的2D快速模式繪圖(2D immediate mode graphic)的標准。許多開發者現在利用它來實現眾多的多媒體項目、可視化醒目以及游戲等等。然而,隨着我們構建的應用程序的復雜度的增加,我們難免會遇到所謂的性能問題。
已經存在眾多優化canvas性能的方法了,但是還沒有一篇文章將這些方法系統的整理並加以分析。本文的目的就在於將這些方法整理、鞏固以使其曾為 開發者們更容易理解、消化、吸收的資源。本文囊括了適用於所有計算機繪圖環境(computer graphics environments)的最基本的優化方法,以及特定於canvas的優化方法。其中特定於canvas的優化方法可能會隨着canvas實現方式的 更新而發生變化。特別的,當瀏覽器開發商實現了canvas GPU 加速時,我們探討的某些優化方法可能會顯得並不是特別有效,這些情況我們會在特定的地方標注出來。
請注意,本文側重點不在於討論HTML5 canvas的用法。如果想了解canvas的具體用法可以參見HTML5 Rocks網站中canvas相關的文章。比如Dive into HTML5 chapter 以及MDN tutorial。
性能測試
為了處理飛速變化着的HTML5 canvas,JSPerf(jsperf.com) 測試證明了我們在文中提到的每一個方法目前都還生效。JSPerf是一個十分有用的web應用程序,web開發者們可以利用此程序編寫 JavaScript 性能測試用例。每一個測試用例只關注你企圖達到的某一方面的結果(比如說清楚畫布),每一個這樣的測試用例包含有若干個能夠達到同一結果的不同方法。 JSPerf在一小段時間內盡可能多的運行每一個方法,並沒給出一個統計學上有顯著意義的每秒中迭代次數。高分意味着更高的性能。
瀏覽者可以在自己的瀏覽器中打開JSPerf 性能測試頁面,而且可以讓JSPerf把標准化的測試結果存儲在Browserscope(broserscope.org)中。因為本文中提到的優化技術已經備份到了JSPerf的結果中,因此你可以重新運行查看最新的信息以確定相應的方法是否還有效。我已經寫了一個小的幫助應用程序(helper applicatin)來將測試的結果繪制成圖表嵌入到整篇文章中。
本文中的性能測試結果與特定的瀏覽器版本有很重要的關系。由於我們不知到您用的瀏覽器運行在什么操作系統上,更重要的是也不知道在您進行這些測試時 HTML5 canvas是否被硬件加速。你可以在Chrome瀏覽器地址欄中使用 about:gpu 命令來查看Chrome的HTML5 canvas 是否被硬件加速。
1.PRE-RENDER TO AN OFF-SCREEN CANVAS
我們在寫一個游戲的時候常常會遇到在多個連續的幀中重繪相似的物體的情況。在這中情況下,你可以通過預渲染場景中的大部分物體來獲取巨大的性能提 升。預渲染即在一個或者多個臨時的不會在屏幕上顯示的canvas中渲染臨時的圖像,然后再把這些不可見的canvas作為圖像渲染到可見的canvas 中。對於計算機圖形學比較熟悉的朋友應該都知道,這個技術也被稱做display list。
例如,假定你在重繪以每秒60幀運行的Mario。你既可以在每一幀重繪他的帽子、胡子和“M”也可以在運行動畫前預渲染Mario。
沒有預渲染的情況:
- // canvas, context are defined
- function render() {
- drawMario(context);
- requestAnimationFrame(render);
- }
預渲染的情況:
- var m_canvas = document.createElement('canvas');
- m_canvas.width = 64;
- m_canvas.height = 64;
- var m_context = m_canvas.getContext(‘2d’);
- drawMario(m_context);
- function render() {
- context.drawImage(m_canvas, 0, 0);
- requestAnimationFrame(render);
- }
關於requestAnimationFrame的使用方法將在后續部分做詳細的講述。下面的圖標說明了顯示了利用預渲染技術所帶來的性能改善情況。(來自於jsperf):

當渲染操作(例如上例中的drawmario)開銷很大時該方法將非常有效。其中很耗資源的文本渲染操作就是一個很好的例子。從下表你可以看到利用預渲染操作所帶來的強烈的性能提升。(來自於jsperf):

然而,觀察上邊的例子我們可以看出松散的預渲染(pre-renderde loose)性能很差。當使用預渲染的方法時,我們要確保臨時的canvas恰好適應你准備渲染的圖片的大小,否則過大的canvas會導致我們獲取的性 能提升被將一個較大的畫布復制到另外一個畫布的操作帶來的性能損失所抵消掉。
上述的測試用例中緊湊的canvas相當的小:
- can2.width = 100;
- can2.height = 40;
如下寬松的canvas將導致糟糕的性能:
- can3.width = 300;
- can3.height = 100;
2.BATCH CANVAS CALLS TOGETHER
因為繪圖是一個代價昂貴的操作,因此,用一個長的指令集載入將繪圖狀態機載入,然后再一股腦的全部寫入到video緩沖區。這樣會會更佳有效率。
例如,當需要畫對條線條時先創建一條包含所有線條的路經然后用一個draw調用將比分別單獨的畫每一條線條要高效的多:
- or (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();
- }
通過繪制一個包含多條線條的路徑我們可以獲得更好的性能:
- ontext.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();
這個方法也適用於HTML5 canvas。比如,當我們畫一條復雜的路徑時,將所有的點放到路徑中會比分別單獨的繪制各個部分要高效的多(jsperf):

然而,需要注意的是,對於canvas來說存在一個重要的例外情況:若欲繪制的對象的部件中含有小的邊界框(例如,垂直的線條或者水平的線條),那么單獨的渲染這些線條或許會更加有效(jsperf):

3.AVOID UNNECESSARY CANVAS STATE CHANGES
HTML5 canvas元素是在一個狀態機之上實現的。狀態機可以跟蹤諸如fill、stroke-style以及組成當前路徑的previous points等等。在試圖優化繪圖性能時,我們往往將注意力只放在圖形渲染上。實際上,操縱狀態機也會導致性能上的開銷。
例如,如果你使用多種填充色來渲染一個場景,按照不同的顏色分別渲染要比通過canvas上的布局來進行渲染要更加節省資源。為了渲染一副條紋的圖案,你可以這樣渲染:用一種顏色渲染一條線條,然后改變顏色,渲染下一條線條,如此反復:
- 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);
- }
下面的性能測試用例分別用上邊兩種方法繪制了一副交錯的細條紋圖案(jsperf):

正如我們預期的,交錯改變狀態的方法要慢的多,原因是變化狀態機是有額外開銷的。
4.RENDER SCREEN DIFFERENCES ONLY, NOT THE WHOLE NEW STATE
這個很容易理解,在屏幕上繪制較少的東西要比繪制大量的東西節省資源。重繪時如果只有少量的差異你可以通過僅僅重繪差異部分來獲得顯著的性能提升。換句話說,不要在重繪前清除整個畫布。:
- context.fillRect(0, 0, canvas.width, canvas.height);
跟蹤已繪制部分的邊界框,僅僅清理這個邊界之內的東西:
- context.fillRect(last.x, last.y, last.width, last.height);
下面的測試用例說明了這一點。該測試用例中繪制了一個穿過屏幕的白點(jsperf):

如果您對計算機圖形學比較熟悉,你或許應該知道這項技術也叫做“redraw technique”,這項技術會保存前一個渲染操作的邊界框,下一次繪制前僅僅清理這一部分的內容。
這項技術也適用於基於像素的渲染環境。這篇名為JavaScript NIntendo emulator tallk的文章說明了這一點。
5.USE MUTIPLE LAYERED CANVASES FOR COMPLEX SCENES
我們前邊提到過,繪制一副較大的圖片代價是很高昂的因此我們應盡可能的避免。除了前邊講到的利用另外得不可見的canvas進行預渲染外,我們也可以疊在 一起的多層canvas。圖哦你的過利用前景的透明度,我們可以在渲染時依靠GPU整合不同的alpha值。你可以像如下這么設置,兩個絕對定位的 canvas一個在另一個的上邊:
- <canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
- </canvas>
- <canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
- </canvas>
相對於僅僅有一個canvas的情況來講,這個方法的優勢在於,當我們需要繪制或者清理前景canvas時,我們不需要每次都修改背景 canvas。如果你的游戲或者多媒體應用可以分成前景和背景這樣的情況,那么請考慮分貝渲染前景和背景來獲取顯著的性能提升。下面的圖表比較了只有一個 canvas的情況和有前景背景兩個canvas而你只需要清理和重繪前景的情況(jsperf):

你可以用相較慢的速度(相對於前景)來渲染背景,這樣便可利用人眼的一些視覺特性達到一定程度的立體感,這樣會更吸引用戶的眼球。比如,你可以在每一幀中渲染前景而僅僅每N幀才渲染背景。
注意,這個方法也可以推廣到包含更多canvas曾的復合canvas。如果你的應用利用更多的曾會運行的更好時請利用這種方法。
6.AVOID SHADOWBLUR
跟其他很多繪圖環境一樣,HTML5 canvas允許開發者對繪圖基元使用陰影效果,然而,這項操作是相當耗費資源的。
- context.shadowOffsetX = 5;
- context.shadowOffsetY = 5;
- context.shadowBlur = 4;
- context.shadowColor = 'rgba(255, 0, 0, 0.5)';
- context.fillRect(20, 20, 150, 100);
下面的測試顯示了繪制同一場景使用何不使用陰影效果所帶來的顯著的性能差異(jsperf):

7.KNOW VARIOUS WAYS TO CLEAR THE CANVAS
因為HTML5 canvas 是一種即時模式(immediate mode)的繪圖范式(drawing paradigm),因此場景在每一幀都必需重繪。正因為此,清楚canvas的操作對於 HTML5 應用或者游戲來說有着根本的重要性。
正如在 避免 canvas 狀態變化的一節中提到的,清楚整個canvas的操作往往是不可取的。如果你必須這樣做的話有兩種方法可供選擇:調用
- context.clearRect(0, 0, width, height)
或者使用 canvas特定的一個技巧
- canvas.width = canvas.width
在書寫本文的時候,cleaRect方法普遍優越於重置canvas寬度的方法。但是,在某些情況下,在Chrome14中使用重置canvas寬度的技巧要比clearRect方法快很多(jsperf):

請謹慎使用這一技巧,因為它很大程度上依賴於底層的canvas實現,因此很容易發生變化,欲了解更多信息請參見 Simon Sarris 的關於清除畫布的文章。
8.AVOID FLOATING POINT COORDINATES
HTML5 canvas 支持子像素渲染(sub-pixel rendering),而且沒有辦法關閉這一功能。如果你繪制非整數坐標他會自動使用抗鋸齒失真以使邊緣平滑。以下是相應的視覺效果(參見Seb Lee-Delisle的關於子像素畫布性能的文章)

如果平滑的精靈並非您期望的效果,那么使用 Math.floor方法或者Math.round方法將你的浮點坐標轉換成整數坐標將大大提高運行速度(jsperf):

為使浮點坐標抓換為整數坐標你可以使用許多聰明的技巧,其中性能最優越的方法莫過於將數值加0.5然后對所得結果進行移位運算以消除小數部分。
- // With a bitwise or.
- rounded = (0.5 + somenum) | 0;
- // A double bitwise not.
- rounded = ~~ (0.5 + somenum);
- // Finally, a left bitwise shift.
- rounded = (0.5 + somenum) << 0;
兩種方法性能對比如下(jsperf):

9.OPTIMIZE YOUR ANIMATIONS WITH ‘REQUESTANIMATIONFRAME’
相對較新的 requeatAnimationFrame API是在瀏覽器中實現交互式應用的推薦標准。與傳統的以固定頻率命令瀏覽器進行渲染不同,該方法可以更友善的對待瀏覽器,它會在瀏覽器可用的時候使其來 渲染。這樣帶來的另外一個好處是當頁面不可見的時候,它會很聰明的停止渲染。
requestAnimationFrame調用的目標是以60幀每秒的速度來調用,但是他並不能保證做到。所以你要跟蹤從上一次調用導線在共花了多長時間。這看起來可能如下所示:
- var x = 100;
- var y = 100;
- var lastRender = new Date();
- function render() {
- var delta = new Date() - lastRender;
- x += delta;
- y += delta;
- context.fillRect(x, y, W, H);
- requestAnimationFrame(render);
- }
- render();
注意requestAnimationFrame不僅僅適用於canvas 還適用於諸如WebGL的渲染技術。
在書寫本文時,這個API僅僅適用於Chrome,Safari以及Firefox,所以你應該使用這一代碼片段
MOST MOBILE CANVAS IMPLEMENTATION ARE SLOW
讓我們來討論一下移動平台。不幸的是在寫這篇文章的時候,只有IOS 5.0beta 上運行的Safari1.5擁有GPU加速的移動平台canvas實現。如果沒有GPU加速,移動平台的瀏覽器一般沒有足夠強大的CPU來處理基於 canvas的應用。上述的JSperf測試用例在移動平台的運行結果要比桌面型平台的結果糟糕很多。這極大的限制了跨設備類應用的成功運行。
CONCLUSION
j簡要的講,本文較全面的描述了各種十分有用優化方法以幫助開發者開發住性能優越的基於HTML5 canvas的項目。你已經學會了一些新的東西,趕緊去優化你那令人敬畏的創造吧!如果你還沒有創建過一個應用或者游戲,那么請到Chrome Experiment 和Creative JS看看吧,這里能夠激發你的靈感。
來源: http://blog.csdn.net/zyz511919766/article/details/7401792
