提升 HTML5 畫布效能

史穆斯 (Boris Smus)
Boris Smus

引言

HTML5 畫布從 Apple 最初推出的實驗開始,是網路上最廣受支援的 2D 立即模式圖形標準。許多開發人員現在都仰賴這個程式碼處理各種多媒體專案、圖表和遊戲。不過,隨著我們建構的應用程式變得越來越複雜,開發人員不小心也遇到了效能牆問題。有很多關於最佳化畫布效能的線索。本文旨在協助開發人員將部分主體整合成更易於理解的資源。本文提供適用於所有電腦圖形環境的基本最佳化功能,以及隨著畫布實作改善而隨時可能變動的畫布專屬技術。尤其是,當瀏覽器廠商實作畫布 GPU 加速功能時,某些本文所述的效能技巧可能會降低的影響。我們會在適當時機註記這一點。 請注意,本文的目標並非使用 HTML5 畫布。為此,請參閱 HTML5Rocks 的畫布相關文章深入探索 HTML5 網站MDN 畫布的教學課程。

效能測試

為了因應瞬息萬變的 HTML5 畫布世界,JSPerf (jsperf.com) 測試會驗證每項建議的最佳化功能是否仍然有效。JSPerf 是一款網頁應用程式,可讓開發人員編寫 JavaScript 效能測試。每項測試都著重於您想達成的結果 (例如清除畫布),並包含可達到相同結果的多種方法。JSPerf 會在短時間內盡可能多次執行每種方法,並提供具統計意義的每秒疊代次數。分數越高越好! JSPerf 效能測試頁面的訪客可以在瀏覽器上執行測試,並讓 JSPerf 將正規化測試結果儲存在 Browserscope (browserscope.org) 上。本文中的最佳化技巧是由 JSPerf 結果備份,因此您可以回頭查看該技術是否仍適用的最新資訊。我編寫了一款小型輔助應用程式,可將這些結果以圖表形式呈現,並嵌入本文的整段時間。

本文中的所有效能結果都會以瀏覽器版本金鑰設定,這就是一項限制,因為我們不知道瀏覽器執行的是哪個作業系統,更重要的是,不論 HTML5 畫布是否在執行效能測試時進行硬體加速。您可以前往網址列的 about:gpu,確認 Chrome 的 HTML5 畫布是否會進行硬體加速。

預先算繪為螢幕外畫布

如果您希望在多個影格 (例如編寫遊戲時) 重新繪製類似的畫面,可以預先算繪場景的大部分,大幅提高效能。預先算繪是指使用獨立的螢幕外畫布 (或畫布) 算繪暫存圖片,然後將螢幕外的畫布重新轉譯至可見圖片上。 例如,假設您在每秒 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;

批次處理 Canvas 呼叫

由於繪圖作業會耗用大量資源,因此使用大量指令載入繪圖狀態機器,然後將其轉儲到影片緩衝區上,是更有效率的做法。

舉例來說,如要繪製多條線條,建立一條包含所有線條的路徑,然後使用單一繪圖呼叫來繪製,會更有效率。換句話說,您不用分行繪製:

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) 或使用畫布專屬駭客進行操作:canvas.width = canvas.width。撰寫當時,clearRect 通常會比寬度重設版本還快,但在某些情況下,在 Chrome 14 中使用 canvas.width 重設駭客會大幅縮短

請謹慎處理這項秘訣,因為這項提示高度仰賴基礎畫布實作,而且可能會大幅變更。詳情請參閱 Simon Sarris 關於清除畫布的文章

避免使用浮點座標

HTML5 畫布支援子像素算繪,且無法關閉。如果您使用非整數的座標繪製,系統會自動使用反鋸齒選項,嘗試讓線條看起來更加平滑。這是從Seb Lee-Delisle 撰寫的子像素畫布效能文章中擷取的視覺效果:

子像素

如果平滑的 Sprite 不是您需要的效果,建議使用 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 加速 (可快速轉譯非整數座標),這種最佳化方式就不再需要使用。

使用 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,因此建議您使用這個填充碼

多數行動裝置畫布的導入速度都很慢

現在來談談行動裝置很抱歉,在本文撰寫期間,只有執行 Safari 5.1 的 iOS 5.0 Beta 版才能夠執行 GPU 加速行動裝置畫布實作。如果沒有 GPU 加速功能,行動瀏覽器通常效能不彰,無法用於新型畫布應用程式。與電腦相比,上述多項 JSPerf 測試會在行動裝置上執行顯得更糟糕的規模,大幅限制了可成功執行的跨裝置應用程式類型。

結語

總結來說,本文介紹了一套全方位的實用最佳化技巧,可協助您開發高效能的 HTML5 畫布專案。現在您已經有所收穫,可以開始運用這些技巧了如果您目前沒有可以最佳化的遊戲或應用程式,請參考 Chrome 實驗廣告素材 JS 來汲取靈感。

參考資料

  • 「即時」模式與「保留」模式。
  • 其他 HTML5Rocks 畫布文章
  • 「深入瞭解 HTML5」的畫布部分
  • JSPerf 可讓開發人員建立 JS 效能測試。
  • Browserscope 會儲存瀏覽器成效資料。
  • JSPerfView:會將 JSPerf 測試算繪為圖表。
  • 阿蒙撰寫網誌文章,主題是如何清除無框畫,而他的書籍《HTML5 Unleashed》又收錄了 Canvas 的表現章節。
  • Sebastian 的網誌文章,瞭解子像素的轉譯效能。
  • 瞭如何最佳化 JS NES 模擬器。
  • Chrome 開發人員工具中的全新 canvas 分析器