HTML5 Canvasパフォーマンスチューニング
以前、HTML5 Canvasで表現する打ち上げ花火として、Canvasを使った作品をご紹介しましたが、
PC版Webkitを除く全てのブラウザ(スマホブラウザは勿論、IE9など)で重いという課題がありました。
PCブラウザに限定すれば、ブラウザやハードウェアの進化ととも改善される問題ではありますが、
スマホ(に限らず携帯端末)は2年縛りで購入するユーザーが圧倒的に多く、1・2年前の機種(環境)を使用する状況が続くことが容易に想像できます。
スマホサイトやアプリ制作を手掛けたことのある方はご存知の通り、Android2.xは色々と不都合があるだけでなく
パフォーマンス面でも十分に配慮して制作する必要があります。
(Android4.xではGPUアクセラレーションがサポートされ、期待出来るのですが世代交代は先の話)
そこで、本エントリーではロジックを工夫することで得られたパフォーマンス改善の一部をご紹介します。
目的(目標)
Canvasのレンダリングにおいてボトルネックとなる処理を把握すること。
予め高コストな処理を把握出来ていれば、今後パフォーマンスに配慮したロジックが組めます。
効果のあった施策
試行錯誤する中で、際立ってパフォーマンス改善が見られたもの。
- requestAnimationFrame()を使用する
-
いまや定番ですが、アニメーション目的であればsetTimeout()やsetInterval()よりrequestAnimationFrame()の方が多くの場合で滑らかです。
関数を定義するほどでもない一時的な利用であれば、以下コードのように無名関数でラップして使用しています。// window.requestAnimationFrameをベンダープレフィックス付きで定義 (function( w, r ) { w['r'+r] = w['r'+r] || w['webkitR'+r] || w['mozR'+r] || w['msR'+r] || w['oR'+r] || function(c){ w.setTimeout(c, 1000 / 60); }; })( this, 'equestAnimationFrame' ); var hogeCounter = 0; (function() { if ( 30 < hogeCounter ) { return; } // do something hogeCounter++; requestAnimationFrame( arguments.callee ); })();
- Canvasの状態操作を必要最小限にする
-
ここで言う状態とは、fillStyle()やshadowColor()などの設定を指します。
先日作った打ち上げ花火では、火花(色付きパーティクル)の内側に白いパーティクルを配置することで残像を表現しています。この白いパーティクルを配置する際に、forループの内部で都度色変更を行いループを1回で済ませていましたが、
今回のケースではforループの直前で色変更を行い、ループを2回行った方が高速です。
※配列の長さに左右されますが、一般的にはループは少ない方が速いちなみに残像をarc()ではなくclearRect()で表現する事で色変更の必要がなくなり、forループが1回で済みますが、
arc()を使用して2回ループした以下パターンの方がハイパフォーマンスでした。Math.Radian = Math.PI * 2; /* 重い例 */ for ( var i = -1, l = particles.length; ++i < l; ) { var particle = particles[i]; particle.velX *= this.rate; particle.velY *= this.rate; particle.posX += particle.velX; particle.posY += particle.velY; particle.posY += this.gravity; // 色付きパーティクル canvas.ctx.fillStyle = this.color; canvas.ctx.beginPath(); canvas.ctx.arc( particle.posX, particle.posY, particle.size, 0, Math.Radian, true ); canvas.ctx.fill(); // 白いパーティクル canvas.ctx.fillStyle = '#ffffff'; canvas.ctx.beginPath(); canvas.ctx.arc( particle.posX - ( particle.velX * 4 ), particle.posY - ( particle.velY * 4 ), particle.size - 0.5, 0, Math.Radian, true ); canvas.ctx.fill(); } /* 軽い例 */ // 色付きパーティクル canvas.ctx.fillStyle = this.color; for ( var i = -1, l = particles.length; ++i < l; ) { var particle = particles[i]; particle.velX *= this.rate; particle.velY *= this.rate; particle.posX += particle.velX; particle.posY += particle.velY; particle.posY += this.gravity; canvas.ctx.beginPath(); canvas.ctx.arc( particle.posX, particle.posY, particle.size, 0, Math.Radian, true ); canvas.ctx.fill(); } // 白いパーティクル canvas.ctx.fillStyle = '#ffffff'; for ( var i = -1, l = particles.length; ++i < l; ) { var particle = particles[i]; canvas.ctx.beginPath(); canvas.ctx.arc( particle.posX - ( particle.velX * 4 ), particle.posY - ( particle.velY * 4 ), particle.size - 0.5, 0, Math.Radian, true ); canvas.ctx.fill(); }
制作内容次第で効果のある施策
以下は制作内容次第でパフォーマンス改善が期待出来るが、
今回の打ち上げ花火では採用を見送ったもの。
- 差分のみレンダリングする
-
画面全体を再描画する必要がなければ、部分的にクリアして差分のみレンダリングします。
当然ですが、描画する面積が広いほど負荷が高くなります。// 全体をクリア(NG) canvas.ctx.fillRect( 0, 0, canvas.width, canvas.height ); // 必要な部分のみクリアして再描画(OK) canvas.ctx.fillRect( last.posX, last.posY, last.width, last.height ); canvas.ctx.beginPath(); canvas.ctx.arc( last.posX, last.posY, last.size, 0, Math.Radian, true ); canvas.ctx.fill();
- 浮動小数点座標を整数に変換する
-
Canvasでは整数以外の座標でレンダリングすると、線が滑らかになるよう自動的にアンチエイリアスが使用されます。
滑らかなアニメーションが必要でなければ、Math.round()などで整数に変換した方が負荷が下がり高速です。ただ、整数に変換(四捨五入or切り捨てなど)のデメリットとして、斜め方向のアニメーションが不自然な動きとなりますし、
GPUアクセラレーション対応の環境では整数以外でも高速に動作するようなので、最新の環境では劇的な変化はないということを付け加えておきます。 - 複数のCanvas要素を使ってプレレンダリングやレイヤーを検討する
-
一定の演出の後、繰り返し表示する画面があれば都度クリアして再描画するのではなく、
別のCanvas要素に予め描画しておき、必要に応じてz-indexやdisplayで切り替えた方が高速です。
参考情報
Canvasのパフォーマンス改善手段を色々と調査しましたが日本語での纏まった情報は少なく感じました。
私が見た中では以下のサイトが非常にわかり易く大変参考にさせていただきました。
最後に
パフォーマンス改善前と改善後のデモは以下リンクよりご覧ください。
今回ご紹介したパフォーマンス改善例で、スマホではiOS5.x+Safariは大幅に改善されました。
ただ、Android2.3.xではまだ処理が重く、もう少し改善出来ればといったところです。
今後新たな気づきがあれば追記、もしくは別のエントリーでご紹介します。