簡介
HTML5 畫布最初是 Apple 的實驗項目,是網路上 2D 即時模式圖形最廣泛支援的標準。如今許多開發人員都依賴這個方式處理各種多媒體專案、圖表和遊戲。不過,隨著我們建構的應用程式越來越複雜,開發人員不小心就會碰到效能牆。將畫布效能最佳化時,有很多缺點。本文旨在將這部分內容整合為開發人員更容易消化的資源。本文包含適用於所有電腦圖形環境的基本最佳化技巧,以及隨著畫布實作改善而可能變更的畫布專屬技巧。特別是,隨著瀏覽器供應商導入畫布 GPU 加速功能,上述提及的部分效能技巧可能就會失效。我們會在適當的地方註明這項資訊。請注意,本文不會說明 HTML5 畫布的用法。為此,請參閱 HTML5Rocks 上的畫布相關文章、深入探索 HTML5 網站或 MDN 畫布的教學課程。
效能測試
為了因應快速變化的 HTML5 畫布,JSPerf (jsperf.com) 測試會驗證每項最佳化建議是否仍有效。JSPerf 是網路應用程式,可讓開發人員編寫 JavaScript 效能測試。每項測試都著重於您想達成的結果 (例如清理畫布),並包含多個方法可以達到相同結果。JSPerf 會在短時間內盡可能執行每個方法,並提供每秒疊代次數的統計顯著數字。分數越高越好!造訪 JSPerf 效能測試頁面的訪客可以在瀏覽器上執行測試,並讓 JSPerf 將正規化的測試結果儲存在 Browserscope (browserscope.org) 中。由於本文中的最佳化技巧有 JSPerf 結果做為後盾,您可以返回查看最新資訊,瞭解該技巧是否仍適用。我編寫了一個小型輔助應用程式,會將這些結果內嵌為圖表,並嵌入本文中。
本文中的所有效能結果都以瀏覽器版本為依據。這項限制是因為我們不知道瀏覽器執行的是哪個作業系統,更重要的是,我們不知道在效能測試執行時,HTML5 畫布是否使用硬體加速功能。如要瞭解 Chrome 的 HTML5 畫布是否啟用硬體加速功能,請在網址列中輸入 about:gpu
。
預先轉譯至螢幕外畫布
如果在多個影格之間重新繪製類似的原始版本 (就像編寫遊戲時經常出現的情況),就可以預先轉譯場景中的大部分,大幅提升效能。預先算繪是指使用單獨的離螢幕畫布 (或多個畫布) 算繪暫時圖片,然後將離螢幕畫布算繪回可見的畫布。舉例來說,假設您以每秒 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
的用法,我們會在後續章節中詳細說明。
在算繪作業 (上例中的 drawMario
) 成本高昂時,這項技巧特別有效。文字轉譯就是一個很好的例子,這是相當昂貴的作業。
不過,「預先算繪鬆散」測試案例的效能不佳。進行預先算繪時,請務必確保臨時畫布與您繪製的圖片相符,否則,在螢幕外算繪所帶來的效能提升,會因將一個大型畫布複製到另一個畫布而導致效能損失 (這會因來源目標大小而異)。上方測試中,緊密畫布只是較小:
can2.width = 100;
can2.height = 40;
相較於產生效能較差的寬鬆測試:
can3.width = 300;
can3.height = 100;
將畫布呼叫批次處理
由於繪圖是耗用資源的作業,因此使用長串指令載入繪圖狀態機器,然後將所有指令轉儲至影片緩衝區,會更有效率。
舉例來說,繪製多條線條時,建立包含所有線條的路徑,並透過單一繪圖呼叫來繪製,會更有效率。換句話說,請不要繪製個別的線條:
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();
這也適用於 HTML5 畫布的世界。舉例來說,繪製複雜路徑時,最好將所有點放進路徑中,而不是分別算繪線段 (jsperf)。
不過,請注意,此規則在 Canvas 中有一項重要的例外狀況:如果繪製所需物件的原始元素具有較小的邊界框 (例如水平和垂直線),實際上可能更有效率地個別轉譯這些元素 (jsperf)。
避免不必要的畫布狀態變更
HTML5 畫布元素是在狀態機器上實作,該機器會追蹤填充和筆劃樣式等項目,以及組成目前路徑的先前點。在嘗試最佳化圖像效能時,您可能會想專注於圖像算繪。不過,操控狀態機器也可能會造成效能額外負擔。舉例來說,如果您使用多種填充顏色來算繪場景,以顏色算繪會比在畫布上算繪更省錢。如要算繪細條紋圖案,您可以算繪條紋、變更顏色、算繪下一個條紋等:
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);
}
如預期,由於變更狀態機器的成本高昂,因此交錯方法的速度較慢。
只算繪畫面差異,而非整個新狀態
如您所料,在螢幕上顯示的內容越少,渲染成本就越低。如果重繪之間只有漸進差異,只要繪製差異,就能大幅提升效能。換句話說,與其在繪圖前清除整個畫面:
context.fillRect(0, 0, canvas.width, canvas.height);
持續追蹤繪製的定界框,只清除這些資料。
context.fillRect(last.x, last.y, last.width, last.height);
如果您熟悉電腦圖形,可能也知道這項技巧稱為「重繪區域」,其中會儲存先前算繪的邊界框,然後在每次算繪時清除。這項技巧也適用於以像素為基礎的轉譯內容,如這篇 JavaScript Nintendo 模擬器講座所示。
使用多個分層畫布處理複雜場景
如前所述,繪製大型圖片的成本很高,因此應盡量避免。除了使用另一個畫布來轉譯螢幕外內容 (如預先轉譯部分所述),我們也可以使用彼此堆疊的畫布。在前景畫布中使用透明度,我們就能在算繪期間依賴 GPU 將 alpha 組合在一起。您可以按照下列方式設定此做法,讓兩個絕對位置的畫布疊在一起。
<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>
有別於這裡只有一個畫布的優點,就是在繪製或清除前景畫布時,我們絕不會修改背景。如果您的遊戲或多媒體應用程式可分為前景和背景,建議您在不同的畫布上算繪這些內容,以大幅提升效能。
您通常可利用不完美的人類觀感,只需以與前景相比的速度,以較慢的速度或較慢的速度轉譯背景,因為這很可能會佔據大部分的使用者註意力。舉例來說,您可以在每次轉譯時算繪前景,但只在每 N 個影格算繪背景。另外請注意,如果應用程式與這類結構搭配得更好,這種做法就能將任何數量的複合畫布推廣至一般情況。
避免使用陰影模糊
與許多其他圖形環境一樣,HTML5 畫布可讓開發人員模糊處理基本元素,但這項作業可能會耗費大量資源:
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);
瞭解清除畫布的各種方式
由於 HTML5 畫布是即時模式繪圖典範,因此需要在每個影格中明確重繪場景。因此,清除畫布對 HTML5 畫布應用程式和遊戲而言,是相當重要的操作。如「避免畫布狀態變更」一節所述,您通常不希望清除整個畫布,但如果您「必須」執行這項作業,有兩種做法:呼叫 context.clearRect(0, 0, width, height)
或使用畫布專用 Hack 進行:canvas.width = canvas.width
。在本文撰寫期間,clearRect
通常會比在 Chrome 14 中使用 canvas.width
入侵來重設寬度重設版本的速度明顯加快。
請謹慎處理這項提示,因為這主要取決於基礎畫布實作,而且可能會隨時變動。詳情請參閱 Simon Sarris 關於清除畫布內容的文章。
避免使用浮點座標
HTML5 畫布支援子像素算繪,且無法關閉。如果您使用非整數的座標繪圖,系統會自動使用反鋸齒效果,嘗試讓線條更平滑。以下是視覺效果,摘自 Seb Lee-Delisle 撰寫的這篇關於子像素畫布效能的文章:
如果您不想要使用平滑的圖像,可以使用 Math.floor
或 Math.round
(jsperf) 將座標轉換為整數,速度會快得多:
如要將浮點座標轉換為整數,您可以使用一些巧妙的技術,其中成效最出色的方法包括在目標編號中加入半數,然後對結果執行位元運算來消除部分元素。
// 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)。
請注意,一旦將畫布實作項目加速至 GPU,這類最佳化就不再重要,因為 GPU 可快速算繪非整數座標。
使用 requestAnimationFrame
最佳化動畫
在瀏覽器中實作互動式應用程式時,建議使用相對較新的 requestAnimationFrame
API。您可以請瀏覽器在可用時呼叫算繪例行程序,而非指揮瀏覽器以特定固定的時間間隔算繪。這項功能的附帶好處是,如果網頁不在前景中,瀏覽器會聰明地不進行轉譯。requestAnimationFrame
回呼的目標是 60 FPS 回呼率,但無法保證達到這個目標,因此您需要追蹤上次轉譯後經過了多久的時間。如下所示:
var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
var delta = Date.now() - lastRender;
x += delta;
y += delta;
context.fillRect(x, y, W, H);
requestAnimationFrame(render);
}
render();
請注意,這種使用 requestAnimationFrame
適用於畫布和其他轉譯技術,例如 WebGL。在撰寫本文時,這個 API 僅適用於 Chrome、Safari 和 Firefox,因此您應使用這個 shim。
大部分行動裝置畫布的導入速度都很慢
讓我們談談行動廣告。不幸的是,在撰寫本文時,只有執行 Safari 5.1 的 iOS 5.0 Beta 版支援 GPU 加速的行動畫布實作。沒有 GPU 加速功能的話,行動瀏覽器通常沒有足夠強大的 CPU,無法支援以新式畫布為基礎的應用程式。與電腦版相比,上述許多 JSPerf 測試在行動裝置上的效能差了好幾個數量級,因此您可以執行的跨裝置應用程式類型將受到嚴重限制。
結論
總結來說,本文涵蓋了一系列實用的最佳化技巧,有助您開發效能出色的 HTML5 以畫布為基礎的專案。在瞭解了這項新功能後,請盡情發揮創意,製作出精彩的內容。如果您目前沒有要最佳化的遊戲或應用程式,不妨參考 Chrome 實驗和 Creative JS 的內容。
參考資料
- 立即模式與保留模式的比較。
- 其他 HTML5Rocks 畫布文章。
- 深入瞭解 HTML5 的畫布部分。
- JSPerf 可讓開發人員建立 JS 效能測試。
- Browserscope 會儲存瀏覽器效能資料。
- JSPerfView:以圖表呈現 JSPerf 測試結果。
- 請參閱 Simon 的部落格文章,瞭解如何清除畫布,以及他的書籍 HTML5 Unleashed,其中包含有關 Canvas 效能的章節。
- Sebastian 的網誌文章,討論子像素算繪效能。
- Ben 的演講,主題是如何最佳化 JS NES 模擬器。
- Chrome 開發人員工具中的全新 canvas 分析器。