提升 HTML5 畫布效能

Boris Smus
Boris Smus

簡介

HTML5 畫布最初是 Apple 的實驗項目,是網路上 2D 即時模式圖形最廣泛支援的標準。許多開發人員現在都會在各種多媒體專案、視覺化效果和遊戲中使用這個 API。不過,隨著我們建構的應用程式越來越複雜,開發人員不經意地就會碰到效能牆。關於如何最佳化畫布效能的知識,目前有許多不相關的說法。本文旨在將這部分內容整合為開發人員更容易消化的資源。本文包含適用於所有電腦圖形環境的基本最佳化技巧,以及隨著畫布實作改善而可能變更的畫布專屬技巧。特別是,隨著瀏覽器供應商導入畫布 GPU 加速功能,上述提及的部分效能技巧可能就會失效。我們會在適當的地方註明這項資訊。請注意,本文不會說明 HTML5 畫布的用法。如要瞭解這項功能,請參閱 HTML5Rocks 上的畫布相關文章深入瞭解 HTML5 網站的這一章節,或參閱 MDN Canvas 教學課程。

效能測試

為了因應快速變化的 HTML5 畫布,JSPerf (jsperf.com) 測試會驗證每項最佳化建議是否仍有效。JSPerf 是網路應用程式,可讓開發人員編寫 JavaScript 效能測試。每項測試都會著重於您要達成的結果 (例如清除畫布),並包含多種可達到相同結果的方法。JSPerf 會在短時間內盡可能執行每個方法,並提供每秒疊代次數的統計顯著數字。分數越高越好!造訪 JSPerf 效能測試頁面的訪客可以在瀏覽器上執行測試,並讓 JSPerf 將標準化測試結果儲存至 Browserscope (browserscope.org)。由於本文中的最佳化技巧有 JSPerf 結果做為後盾,您可以返回查看最新資訊,瞭解該技巧是否仍適用。我編寫了一個小型輔助應用程式,可將這些結果以圖表呈現,並嵌入本文中。

本文中的所有效能結果都以瀏覽器版本為依據。這項限制是因為我們不知道瀏覽器執行的是哪個作業系統,更重要的是,我們不知道在效能測試執行時,HTML5 畫布是否使用硬體加速功能。如要瞭解 Chrome 的 HTML5 畫布是否啟用硬體加速功能,請在網址列中輸入 about:gpu

預先轉譯至螢幕外無框畫

如果您在多個影格中重繪類似的圖元到螢幕上 (這在編寫遊戲時經常發生),您可以透過預先算繪場景的大部分內容,大幅提升效能。預先算繪是指使用單獨的離螢幕畫布 (或多個畫布) 算繪暫時圖片,然後將離螢幕畫布算繪回可見的畫布。舉例來說,假設您以每秒 60 張影格的速度重繪 Mario 跑步的畫面,您可以在每個影格重新繪製他的帽子、小鬍子和「M」字,也可以在執行動畫前預先算繪馬力歐。不使用預先算繪:

// 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 個影格算繪背景。另外請注意,如果應用程式更適合使用這類結構,這種做法可將任何數量的複合畫布推廣至一般情況。

避免使用 shadowBlur

與許多其他圖形環境一樣,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) 或使用特定畫布的駭客攻擊:canvas.width = canvas.width。在撰寫本文時,clearRect 通常比寬度重設版本更優異,但在某些情況下,使用 canvas.width 重設駭客攻擊在 Chrome 14 中會大幅加快速度

請小心使用這項提示,因為它會嚴重依賴底層畫布實作,且很可能會變更。詳情請參閱 Simon Sarris 關於清除畫布內容的文章

避免使用浮點座標

HTML5 畫布支援子像素算繪,且無法關閉。如果您使用非整數的座標繪圖,系統會自動使用反鋸齒效果,嘗試讓線條更平滑。以下是視覺效果,摘自 Seb Lee-Delisle 撰寫的這篇關於子像素畫布效能的文章

子像素

如果您不想要使用平滑的圖像,可以使用 Math.floorMath.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」的「Canvas」部分
  • JSPerf 可讓開發人員建立 JS 效能測試。
  • Browserscope 會儲存瀏覽器效能資料。
  • JSPerfView:以圖表呈現 JSPerf 測試結果。
  • 請參閱 Simon 的部落格文章,瞭解如何清除畫布,以及他的書籍 HTML5 Unleashed,其中包含有關 Canvas 效能的章節。
  • Sebastian 的網誌文章,討論子像素算繪效能。
  • Ben 的演講,主題是如何最佳化 JS NES 模擬器。
  • Chrome 開發人員工具中的新畫布剖析器