HTML5 Canvas のパフォーマンスの改善

はじめに

Apple の試験運用から始まった HTML5 キャンバスは、ウェブ上で最も広くサポートされている 2D 即時モード グラフィックの標準です。今では多くのデベロッパーが、さまざまなマルチメディア プロジェクト、可視化、ゲームに活用しています。しかし、Google が構築するアプリケーションの複雑さが増すと、デベロッパーがうっかりパフォーマンスの壁にぶつかります。キャンバスのパフォーマンスの最適化については、多くの知恵が切り離されています。この記事では、この本文の一部を、デベロッパー向けのわかりやすいリソースにまとめます。この記事では、すべてのコンピュータ グラフィック環境に適用される基本的な最適化と、キャンバスの実装の改善に伴って変更される可能性があるキャンバス固有の手法について説明します。特に、ブラウザ ベンダーが canvas GPU アクセラレーションを実装すると、ここで説明したパフォーマンス手法の一部の効果は低下する可能性があります。必要に応じて、この点に注意してください。なお、この記事では HTML5 キャンバスの使用については説明しません。詳しくは、HTML5Rocks のキャンバス関連の記事Dive into HTML5 サイトの章MDN Canvas チュートリアルをご覧ください。

パフォーマンス テスト

めまぐるしく変化する HTML5 キャンバスに対応するために、JSPerfjsperf.com)のテストでは、提案されたすべての最適化がまだ機能していることを確認します。JSPerf は、デベロッパーが JavaScript のパフォーマンス テストを作成できるようにするウェブ アプリケーションです。各テストでは、達成しようとしている結果(キャンバスのクリアなど)に焦点を当て、同じ結果を達成する複数のアプローチが含まれます。JSPerf は、各アプローチを短期間でできるだけ多く実行し、1 秒あたりの反復回数を統計的に有意にします。スコアが高いほど良いです。JSPerf パフォーマンス テストページにアクセスすると、ブラウザでテストを実行し、JSPerf が Browserscopebrowserscope.org)に正規化されたテスト結果を保存できます。この記事で紹介する最適化手法は JSPerf の結果に基づいているため、手法が現在も適用されているかどうかに関する最新情報を確認できます。私は、これらの結果をグラフとして表示する小さなヘルパー アプリケーションを作成しました。この記事では、これらの結果をグラフとして埋め込んでいます。

この記事のパフォーマンス結果はすべて、ブラウザのバージョンに基づくものです。これは、ブラウザが動作していた OS も把握できず、さらに重要なこととして、パフォーマンス テストを実行したときに HTML5 キャンバスがハードウェア アクセラレーションされたかどうかもわからないため、これが制限になります。Chrome の HTML5 キャンバスがハードウェア アクセラレーションに対応しているかどうかは、アドレスバーから about:gpu で確認できます。

画面外キャンバスにプリレンダリングする

ゲームの作成でよくあるように、複数のフレームにわたって同様のプリミティブを画面に再描画する場合は、シーンの大部分を事前レンダリングすることで、パフォーマンスを大幅に向上させることができます。プリレンダリングとは、別の画面外キャンバス(複数可)を使用して一時的な画像をレンダリングし、画面外のキャンバスを表示されている画面上にレンダリングすることです。たとえば、60 フレーム/秒で動作しているマリオを再描画するとします。帽子、口ひげ、「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)にコストがかかる場合に特に効果的です。その良い例がテキスト レンダリングです。これは非常に負荷の高いオペレーションです。

しかし、「プリレンダリングされたルース」のテストケースのパフォーマンスは低いです。プリレンダリングを行う際は、一時キャンバスが描画する画像の周りにぴったり収まるようにすることが重要です。そうしないと、画面外レンダリングのパフォーマンス向上は、1 つの大きなキャンバスを別の大きなキャンバスにコピーすることによるパフォーマンスの低下によって相殺されます(ソース ターゲット サイズの関数によって異なります)。上記のテストで最適なキャンバスは、次のように単純に小さくなります。

can2.width = 100;
can2.height = 40;

パフォーマンスが低下するルーズなモデルとの比較:

can3.width = 300;
can3.height = 100;

キャンバス呼び出しを一括処理する

描画は負荷の高いオペレーションであるため、長いコマンドセットで描画ステートマシンを読み込み、それらすべてを動画バッファにダンプする方が効率的です。

たとえば、複数の線を描画するときは、すべての線を含むパスを 1 つ作成し、1 回の描画呼び出しでそれを描画する方が効率的です。つまり、線を分けるのではなく、次のように記述します。

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 を利用してレンダリング時にアルファを合成できます。これを次のように設定し、2 つのキャンバスを絶対位置で上下に重ねます。

<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>

キャンバスを 1 つだけ使用する場合の利点は、前景のキャンバスを描画またはクリアするときに背景を変更しないことです。ゲームアプリやマルチメディア アプリをフォアグラウンドとバックグラウンドに分割できる場合は、これらを別々のキャンバスにレンダリングしてパフォーマンスを大幅に向上させることを検討してください。

多くの場合、人間の不完全な認識を利用して、背景を 1 回だけ、またはフォアグラウンド(ユーザーの注意のほとんどを占有する可能性が高い)より遅い速度でレンダリングできます。たとえば、前景はレンダリングのたびにレンダリングし、背景は 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 を行うという 2 つの選択肢があります。執筆時点では、通常は clearRect の方が幅のリセット バージョンよりも優れていますが、場合によっては Chrome のリセット ハック 4 よりもはるかに高速です。canvas.width

このヒントは基盤となるキャンバスの実装に大きく依存し、非常に変更される可能性があるため、注意が必要です。詳しくは、キャンバスのクリアに関する Simon Sarris の記事をご覧ください。

浮動小数点座標を避ける

HTML5 キャンバスはサブピクセル レンダリングをサポートしており、これをオフにする方法はありません。整数ではない座標で描画すると、自動的にアンチ エイリアスを使用して線が滑らかになります。視覚効果は次のとおりです。これは、Seb Lee-Delisle によるサブピクセル キャンバスのパフォーマンスに関するこちらの記事から抜粋したものです。

サブピクセル

滑らかなスプライトが目的の効果ではない場合は、Math.floor または Math.roundjsperf)を使用して座標を整数に変換するほうが高速です。

浮動小数点座標を整数に変換するには、いくつかの巧妙な手法を使用できます。最もパフォーマンスの高い手法は、ターゲット数に 1/2 を加算し、その結果に対してビット演算を実行して小数部を削除することです。

// 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 の使用は、Canvas だけでなく WebGL などの他のレンダリング技術にも適用されます。執筆時点でこの API は Chrome、Safari、Firefox でのみ利用できるため、こちらの shim を使用してください。

ほとんどのモバイル キャンバスの実装は遅い

次は、モバイルについてです。残念ながら現時点では、Safari 5.1 を実行している iOS 5.0 ベータ版にのみ GPU アクセラレーションのモバイル キャンバスが実装されています。GPU アクセラレーションがない場合、モバイル ブラウザの CPU は一般に、最新のキャンバス ベースのアプリケーションに対応するほど高性能ではありません。前述の JSPerf のテストの多くでは、モバイルでパソコンと比べてパフォーマンスが桁違いに悪く、正常に実行できるクロスデバイス アプリの種類が大幅に制限されています。

まとめ

この記事では、高パフォーマンスな HTML5 キャンバス ベースのプロジェクトの開発に役立つ、便利な最適化手法を包括的に紹介しました。新しい知識を学んだところで 次は 優れたクリエイティブを最適化しましょう最適化するゲームやアプリケーションをまだお持ちでない場合は、Chrome のテストCreative JS を参考にしていただけます。

参照