近年、ブラウザやブラウザランタイムは、PCやスマートフォンのみならず、テレビ、コンソールゲーム機などの組み込み機器にも導入されるようになりました。また、Raspberry Piに代表されるシングルボードコンピュータも流行り出し、ロースペックな環境で動作しなければならないWebアプリケーション開発の需要が高まろうとしています。
多くの組み込み機器に搭載されたブラウザは、近年よく使われるAPIやCSSをサポートしています。しかし、そのパフォーマンスはスマートフォンと比べて非常に貧弱です。スマートフォンでは当たり前のパフォーマンスが得られることはありません。
本記事では、組込機器のブラウザ事情を紹介し、その上で動作するWebアプリケーションの開発の課題、私の経験での苦労話、そして、それに立ち向かうためのTipsを紹介します。
組込機器とブラウザ
組込機器でWebアプリケーションを開発すると言われても、あまり馴染みがないかもしれません。しかし、すでに身近にブラウザを搭載した機器や、Webアプリケーションをパッケージ化して、ダウンロード・インストール型のWebランタイム環境を提供している機器があります。
もっとも身近な機器といえばテレビでしょう。近年発売されたテレビは、Hybridcastに対応していますが、このHybridcastのコンテンツは、HTML5対応ブラウザランタイム上で動作しており、そのコンテンツはHTML、CSS、JavaScript を使って作られています。
テレビメーカー各社は、スマートTVをよりスマートフォンなどのデバイスとの親和性を高めるために、Web開発者やスマートフォンアプリ開発者には馴染みがあるOSの採用を発表しています。たとえば、日本では販売されていませんが、SamsungはテレビOSにTizenを採用し、すでに一部の国で販売が開始されています。LGはwebOSを採用しており、すでに日本でも販売されています。また、ソニーはAndroidの採用を、パナソニックはFirefox OSの採用を発表済みです。
これらのOSの多くは、アプリをHTML5ベースで開発することができるようになっています。また、Androidも、WebView(アプリから利用できるWebブラウザのランタイム)を使うことで、Webベースのアプリを動かすことが可能になります。
ゲームコンソールもWebと密接に関わるようになっています。Nintendo Wii Uは、ゲームアプリをWebベースで開発できる環境を用意してます。利用者はそれがWebで作られていると意識することなく、そのゲームで遊ぶことができます。
そのほか、近年は、Raspberry Piに代表されるようなシングルボードコンピュータが流行っていますが、当然、こういったデバイスでも、OSにブラウザをインストールすることで、Webコンテンツを表示することが可能です。
このように、Webコンテンツが使われるステージは広がる一方、PC やスマートフォン向けのWebコンテンツと比べて、パフォーマンスはこれまで以上に意識する必要があります。
組込機器ブラウザの問題
テレビやゲームコンソールなどの機器のブラウザは、実は、Web開発者にはお馴染みのオープンソースのブラウザがベースになっています。すでに販売されている機器のブラウザは、WebKitやOpera Prestoがベースになっているものが多いのですが、今後は、Chromium(Blink)やGecko(Firefox)ベースのブラウザを搭載した機器も出てくることでしょう。組込機器といえども、使える機能や互換性の問題については、スマートフォンの状況に近いと言えます。
しかし、組込機器に特有の事情があります。まず、機器のライフサイクルが長いことです。スマートフォンのライフサイクルはおおむね2年程度でしょう。しかし、テレビやゲームコンソールは、そこまで早く買い換えられることはありません。それ以外の用途の組込機器であれば、さらにライフサイクルが長い場合も考えられるでしょう。
Webコンテンツを開発する上で、この機器のライフサイクルが大きく足を引っ張ることがあります。よほど致命的なバグがあれば、バグ改修のアップデートはあるかもしれませんが、基本的にブラウザが機能面でバージョンアップされることは稀です。やや新しい機器だとしても、何世代も前のブラウザだと考えたほうがよいでしょう。
次に大きな問題としては、貧弱なCPUと少ないメモリーです。スマートフォンでは瞬間的に終わるような処理でも、組込機器では時間がかかります。この場合は、もっと洗練されたアルゴリズムを考案する、または、処理を分割するなどの回避策が必要となります。
また、メモリーリークも注意が必要です。組込機器向けのアプリケーションは、メモリーが少ないため、スマートフォンでは問題にならなかったメモリーリークが顕在化しやすくなります。とりわけ、組込み機器向けアプリケーションでは、長時間利用するものも少なくありません。そういったケースにおいては、少しのメモリーリークですら、致命的です。
パフォーマンスとは
一般的にパフォーマンスといえば、起動を早く、処理を速く、アニメーションを滑らかにする、という文脈で語られることが多いといえるでしょう。しかし、組込機器向けアプリケーションでは、さらに、メモリー消費を少なく、CPU処理を少なく、そして、安定的に長時間動作し続ける、という視点が重要になります。
もちろん、すべてが実現できればそれに越したことはありませんが、残念ながら、貧弱なCPUと少ないメモリーでは、すべてを叶えることは物理的にも不可能です。端的に言ってしまえば、何を諦めて捨てるのか、が重要になります。とりわけ、アプリケーション開発の企画の段階で、こういった事情を考慮しておく必要があります。
とはいえ、このような制約がある中でも、アプリケーション開発において、ちょっとした配慮だけで、パフォーマンスが改善する場合があります。以降では、それらの具体的なテクニックについて、ご紹介しましょう。
ページのロード
Webアプリケーションを早く起動するために考慮しなければならないこととして、関連のファイルのダウンロードのデータ量と、ダウンロードするファイルの数が挙げられます。これらを減らすことで、アプリケーションの起動が早くなるだけでなく、ファイルロード時におけるメモリー消費も抑えられます。
ダウンロードするデータ量を減らす方法としては、HTML、CSS、JavaScriptの最小化が効果的です。この最小化は、よくファイル圧縮と言われることがありますが、Zipなどの圧縮アルゴリズムなどを使うものではありません。そのため、Webサーバーとブラウザとの間のHTTP通信に使われるデータ圧縮とは意味が違います。もちろん、HTTP通信における圧縮は最大限活用するほうがよいことは言うまでもありませんが、ここでは、HTML、CSS、JavaScriptの最小化にフォーカスします。
これらのテキストデータは、一般的に改行やインデントが数多く含まれますが、多くの場合、それはなくても構わないものです。とりわけ、JavaScriptはサイズが大きくなりがちですので、最小化の効果も大きいと言えます。さらに JavaScriptでは、構文を解析しながら変数名などを短い名前に変換するなどして、ファイルサイズを小さくするツールもあります。こういったツールを使うと、最小化の効果が増します。
ただし、こういったツールは JavaScript コードを変更します。そのため、はじめからそれを意識してコードを書かないと、最小化した後に動作しなくなる問題が起こりますので注意が必要です。筆者が個人的に好んでいるのは、Google Closure Compiler です。最小化のレベルを選択することも可能です。最小化のレベルを欲張らなければ、最小化を意識せずに書いたコードも問題なく動作する可能性が高くなります。
ダウンロードするファイルの数を減らす方法としては、CSSやJavaScriptをファイルとして分離せずに、HTMLの中に直接書いてしまうのが手っ取り早いでしょう。たとえば、Googleのトップページでは、ほとんどすべてが一つのHTMLの中に書き込まれています。また、CSSスプライトも効果的です。ただし、こういった対策は、コードの保守性を損ないますので、やり過ぎには注意が必要です。
ペイント領域とGPUメモリー
ブラウザ上に表示されたコンテンツに何かしらの変化があると、ブラウザはそれを再描画します。当然ながら、再描画する領域の面積は狭いほど良いわけですが、それを確認することは重要です。描画が行われるということは、メモリーの読み書きが発生することにほかなりません。しかし、組込機器の場合、そのメモリーの読み書きの速度がPCやスマートフォンと比べて遅い場合があります。ましてや、全画面を再描画する、というのは、相当にマシンに負荷をかけることになるため、組込機器においては、これまで以上に描画領域の面積を意識する必要があります。
Chromeであれば、デベロッパーツールから描画領域をリアルタイムに確認することができます。
実際にWebアプリケーションを開発しているときには、書き換えたコンテンツだけが再描画されると思いがちなのですが、描画領域をリアルタイムに見ていると、状況によっては、より広範囲な領域を再描画していることがよく分かります。いくつか例をご紹介しましょう。
下図は、img 要素を使って組み込まれたスマイリー画像をゆっくりと右に移動するアニメーションの一コマをキャプチャーしたものです。この例では、JavaScriptからsetTimeout()を何度も呼び出して、CSSのleftプロパティの値を徐々に増やしています。
この例では、移動のたびに画像の領域が何度も再描画されていることが分かります。一秒間に数十回も再描画するわけですから、それなりに負荷がかかっているはずです。
この移動アニメーションを、CSS TransisionsとCSS TransformsのtranslateX()を使って作りなおした結果は、下図のとおりです。
この結果は、CSSをうまく使うことで、GPUアクセラレーションが有効になり、移動対象となる画像がレイヤーとして処理されていることを表しています。移動の都度、再描画が発生せず、GPUによってレイヤーを移動することでアニメーションが実現されますので、非常に効率的です。
しかし、GPUアクセラレーションにも注意が必要です。組込機器では、GPUのメモリーがPCやスマートフォンと較べて少ない場合が多いと言えます。何でもかんでもGPUアクセラレーションの対象となるようコンテンツを作ってしまうと、かえって不効率になる場合もあります。必要最小限にとどめることも重要です。
次は、単に文字を書き換えるだけの簡単な例です。その代わり、かなりの頻度で文字が書き換えられます。HTMLとJavaScriptは次のとおりです。
1 |
<p>経過時間:<span id="t">0</span>ミリ秒。</p> |
1 2 3 4 5 |
var el = document.querySelector("#t"); (function countUp(now) { el.textContent = Math.round(now); window.requestAnimationFrame(countUp); })(); |
ご覧のとおり、この例では、書き換えたいのは数字の部分だけにも関わらず、行全体が再描画の対象になってしまっています。これを改善するために、少しだけHTMLとCSSの力を借ります。
1 |
<p>経過時間:<span id="outer"><span id="t">0</span></span>ミリ秒。</p> |
1 2 3 4 5 6 7 8 9 10 11 12 |
#outer { display: inline-block; width: 100px; height: 20px; position: relative; } #t { display: inline-block; width: 100px; position: absolute; text-align: right; } |
このようにCSSを使って書き換えコンテンツの幅と高さを固定することで、ブラウザの再描画の領域を固定することができます。
最後に極端な例をご覧いただきましょう。次の例は、ロゴ画像が1秒おきに点滅するだけのシンプルな例です。
1 2 3 4 |
<body> <img id="logo" src="imgs/logo.png"> <footer>...</footer> </body> |
1 2 3 4 5 6 7 |
var el = document.getElementById("logo"); var hidden = false; window.setInterval(function() { hidden = !hidden; el.style.display = hidden ? "none" : "block"; }, 1000); |
ご覧のとおり、再描画の領域が、ブラウザの表示領域全体に及んでしまっています。これは、画像を表示しているimg要素の表示が切り替わる都度、リフローが発生しているからです。実際には書き換える必要がない領域まで、再描画の対象になってしまい、非効率と言えます。これも、CSSの力を少し借りて解決します。
1 2 3 |
#logo { position: absolute; } |
今回は、CSSのpositionプロパティにabsoluteを指定することで、img要素を、ある意味、浮かせます。これによって、リフローが発生しなくなり、img要素だけが再描画の対象になります。
画像のロード
組込デバイスでは、大きな画像をロードする場合にも注意すべきことがあります。特に、img要素の画像のロードが完了したら、次の処理を行いたい場合です。この場合、恐らく次のようなコードを書くのではないでしょうか。
1 2 3 |
imgElement.onload = function() { // 何か次の処理 }; |
実は、画像が大きい場合、img要素で loadイベントが発生しても、その画像のレンダリング処理が終わっていないのです。HTML5 仕様ではこのように定義されています。
If the download was successful and the user agent was able to determine the image’s width and height, […] fire a simple event named load at the img element.
ダウンロードが成功し、ユーザーエージェントが画像の幅と高さを判定できたら、[…]img要素でloadという名前のシンプルなイベントを発出します。
つまり、loadイベントの発生はレンダリング完了を表しているわけではありません。組込機器では、loadイベントの発生のタイミングと、実際にレンダリングが完了したタイミングの差が大きく出る場合があります。仮に、loadイベント発生直後に重い処理を開始してしまうと、処理が重複してしまい、予期せぬ結果を招く可能性があります。
残念ながら、画像のレンダリングが完了したことを表すイベントは存在しません。そのため、不格好ですが、タイマーなどを使って、少しだけ連続する処理を遅らせるのが手っ取り早いでしょう。
1 2 3 4 5 |
imgElement.onload = function() { window.setTimeout(function() { // 何か次の処理 }, 100); }; |
何ミリ秒遅らせるべきかは、対象となるハードウェアのスペックに依存しますので、状況に合わせて微調整が必要となります。
ビデオのロード
HTML5にはvideo要素が導入され、マークアップだけでビデオを埋め込むことができるようになったのはご存知のとおりです。しかし、ビデオを簡単に組み込めるようになったとはいえ、ブラウザに大きな負荷をかけることを意識しなければいけません。とりわけ、ビデオ再生はメモリー消費が大きいため、複数のビデオを組み込む場合には注意が必要です。
video要素にはpreload属性が用意されています。これをうまく使いこなすことが重要となります。
もし、再生開始タイミングのパフォーマンスを気にするなら、preload属性の値をautoにします。
1 |
<video preload="auto"> |
たとえビデオが再生されなくても、バッファリング分のメモリーを消費してしまいます。もし複数のビデオを事前に用意したい場合は注意が必要です。確実にそのビデオが再生されると確信できる状況で使うのが良いでしょう。
もし、メモリー消費の低減を優先したいなら、preload属性の値をnoneにします。
1 |
<video preload="none"> |
この場合は、再生開始のタイミングがかなり遅れることは覚悟しておく必要があります。また、ビデオのサイズ(寸法)や、ビデオの長さの情報は、この時点で取得できません。
もし、どうしても事前にビデオの寸法と長さを知りたいなら、preload属性の値をmetadataにします。
1 |
<video preload="metadata"> |
次に、ビデオ再生の開始の命令を出してから、可能な限り早くビデオが再生できる方法について紹介します。実際にvideo要素を使って再生するビデオファイルは、主にH.264/AAC/MP4 形式が多いのではないでしょうか。MP4形式のコンテナでは、ビデオの長さや寸法などのメタ情報は、ファイルの先頭ではなく最後に格納されます。この場合、ブラウザは、ビデオ再生の直前に、Webサーバーに対して何度もHTTPリクエストを投げることになります。実際にChromeのデベロッパーツールでそれを確認することができます。
ご覧のとおり、再生開始前に3回もWebサーバーにリクエストを送っています。ブラウザはまずファイルの先頭を読み取りにいきます。しかし、そこにメタ情報がないとわかると、今度はファイルの末尾を取得するためにリクエストを送ります。メタ情報が取得できたら、ファイルの先頭に戻ってバッファリング分のビデオデータをダウンロードします。これでやっと、ビデオ再生の準備ができることになります。
MP4形式には、Fast Startという仕組みが用意されています。多くのビデオエンコードソフトには、MP4のオプションとしてFast Startを有効にするかどうかを指定することができます。Fast Startを有効にすると、メタ情報をファイルの末尾ではなく先頭に配置してくれます。こうすることで、ブラウザの読取り回数が1回で済み、再生開始のパフォーマンスが向上します。
メモリー消費
組込機器向けのブラウザ上で動作するアプリケーション開発において、メモリー消費の推移はとても重要です。PCやスマートフォンと比べ、組込機器ではブラウザに割り当てられるメモリーの容量が非常に少ない場合があるからです。PCやスマートフォンではまったく問題にならなかったにも関わらず、組込機器向けに移植すると、メモリー不足でブラウザから警告が表示されたり、場合によっては、ブラウザがクラッシュしてしまいます。また、長時間起動し続けなければならない状況においては、数時間後にメモリー不足に陥ることもあります。このようなシーンでは、少しのメモリーリークでも致命的な結果をもたらします。
組込機器向けWebアプリケーション開発においては、ChromeのデベロッパーツールのTimelineは欠かせません。これはPC上で確認します。組込機器向けのブラウザには、こういったツールが用意されることはないため、実際の状況を把握できるわけではありませんが、少なくとも、PC上でメモリーリークが発生している限り、当然ながら、実機でもメモリーリークが発生しているはずです。
ここで注目すべきポイントは、折れ線グラフで表示されたUsed JS Heap、Documents、Nodes、Listenersの推移です。上図では、Used JS Heap、Nodesの値が、ある時点で下がっていることが分かります。これはJavaScriptエンジンにてガベージコレクションが実行されたことを意味します。これを長時間モニタリングして、ガベージコレクションが実行されるたびに、当初の値に戻ることが理想です。ガベージコレクションが実行されたにも関わらず、これらの値が完全に元に戻らずに、少しずつ増えていく場合は、何かメモリーが開放されない要因があるはずです。
特に、HTML要素を生成と削除する繰り返す場合は、注意が必要です。removeChild()メソッドを使って要素を表すノードをドキュメントから削除しても、上記の折れ線グラフに表示されたNodesの数がガベージコレクション実行後でも減らない場合があります。恐らく、それは、循環参照が残っている可能性があります。また、addEventListener()メソッドやイベントハンドラプロパティを使って、イベントリスナーやイベントハンドラをセットしたにも関わらず、ノードを削除する際に、これらを解除し忘れた場合も該当します。これはChromeデベロッパーツールのListenersの数の推移を見れば把握できます。
ChromeデベロッパーツールのTimelineのUsed JS Heapについて、少し深く見てみましょう。下図は、とあるアプリケーションの Used JS Heapの推移をキャプチャしたものです。
このメモリー消費の推移では、少しずつ上昇しながら、ある時点で降下し、それを頻繁に繰り返しています。つまり、何度もガベージコレクションが発生していることを意味しています。これはよく「ノコギリ型」と言われます。
基本的にガベージコレクションの処理は重いため、その時点でのアニメーションなどを邪魔することがあります。そのため、あまり頻繁にガベージコレクションが発生するのはよくないと言われることがあります。一方、メモリーが少ない環境においては、ブラウザがクラッシュするくらいなら、頻繁にガベージコレクションが発生することで、メモリー消費のピークを抑えるほうがよい場合もあります。どちらがよいかは、状況次第です。
もし頻繁にガベージコレクションを発生させないようにしたいなら、必要な値はすべて変数として事前に用意しておき、それらをできる限り使いまわして、新たな変数を作らないようにします。これはDOMでも同じです。必要なDOMノードを事前に作っておき、それを使いまわします。その代わり、この方法は、実際にそういった変数やDOMノードを使わかなったとしても、メモリーをその分だけ消費してしまいます。
逆にガベージコレクションを早めに発生させたいなら、必要な都度、変数やDOMノードを生成し、不要になった時点で破棄することを繰り返します。パフォーマンスは悪くなりますが、メモリー消費のピークを抑えることができます。
ここでは具体的なテクニックの紹介は伝えきれませんでしたが、メモリーが少ない環境においては、Chromeデベロッパーツールなどのメモリー消費の推移は常に意識しなければいけません。
prototypeの活用
Webアプリケーションを作る際には、コンストラクタ関数を用意し、new演算子を使ってインスタンスを生成することが多いのではないでしょうか。ここでは、ECMAScriptのprototypeを活用するかしないかによって、メモリー消費が違ってくることを紹介します。まずは次のコンストラクタ関数をご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var MyWallet = function(init_price) { this.price = init_price; this.earn = function(price) { this.price += price; if(this.price > 1000) { this.pay(this.price - 1000); } }; this.pay = function(price) { this.price -= price; }; this.look = function() { return this.price; }; }; |
上記のコードでは、コンストラクタ関数の中にメソッドを定義しています。では、次のコードをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var MyWallet = function(init_price) { this.price = init_price; }; MyWallet.prototype.earn = function(price) { this.price += price; if(this.price > 1000) { this.pay(this.price - 1000); } }; MyWallet.prototype.pay = function(price) { this.price -= price; }; MyWallet.prototype.look = function() { return this.price; }; |
今度は、メソッドをprototypeで定義しています。これら2つのコンストラクタは、prototypeを使っているか使っていないかの違いしかなく、事実上、同じ機能を提供します。
では、これらのコンストラクタからインスタンスを大量に生成して、消費メモリーを見てみましょう。いずれも、次のコードを実行します。
1 2 3 4 5 |
var list = []; for( var i=0; i<100000; i++ ) { var w = new MyWallet(0); list.push(w); } |
結果に大きな違いを出すために、現実的ではありませんが、10万個のインスタンスを生成し、それを配列に格納します。この処理が終わった時点でのJS Heap Sizeは次のとおりとなります。
左側はprototypeを使わなかった場合の結果を、右側はprototypeを使った場合の結果を表しています。ご覧のとおり、消費メモリーに倍近い違いがでていることが分かります。
もし、大量にコンストラクタからインスタンスを生成するのであれば、積極的にprototypeを使うのが良いでしょう。
DOMアクセス
最後となりましたが、ここでは、DOMアクセスについて触れます。近年は、jQueryなどのJavaScriptライブラリを使って特定のDOM ノードにアクセスすることが多いのではないでしょうか。しかし、こういったJavaScriptライブラリは便利である反面、使わない機能もテンコ盛りです。しかし、使わなくても、それはブラウザではメモリーに展開されてしまい、無駄にメモリーを消費していることになります。もしJavaScriptライブラリを使うなら、機能を最小限に抑えた軽量なものを使うべき、ということは言うまでもありません。
ここでは、筆者がお気に入りのDOMアクセス用JavaScriptライブラリをご紹介しましょう。その名は、”Vanilla JS“です。
Vanilla JSは、機能を選択して必要な分だけダンロードできるようになっています。
すべての機能にチェックを入れても、gzipでたったの25バイト、展開したら、なんと0バイトです。
このサイトでは、他のJavaScriptライブラリとの速度の比較も掲載されています。
このグラフは、1秒間に、何回、ID指定でノードにアクセスできるかを計測したものです。もちろん、数が多いほどパフォーマンスが良いことを表します。ご覧のとおり、Vanilla JSの速度は、他のJavaScriptライブラリの速度を圧倒しています。
さて、皆さんはもうお気づきですね。Vanilla JSはジョークサイトです。何が言いたいかというと、DOMアクセスを速くしたいなら、JavaScriptライブラリに頼らずに、直接Web標準のDOMで規定されたメソッドを使った方が良いということです。
Web標準のDOMであれば、速度も速い、メモリー消費にも優しい、といいことずくめです。メソッドが長くてコードを書くのが面倒、便利なメソッドがない、というデメリットはありますが、少なくとも、CPUが遅い、メモリーが少ない、といったナイナイ尽くしの組込機器においては、そのデメリットを覆すほどのメリットがあるのです。
これは当然、組込機器にかぎらず、スマートフォンにも当てはまります。あらためて、Web標準のDOMに注目してみてはいかがでしょう。
まとめ
ここではCPUが遅い、メモリーが少ない環境でも、ちょっとした配慮でパフォーマンスの効果が出るトピックを紹介してきました。
近年、さまざまなJavaScriptライブラリ、プログラミング手法、アルゴリズムなどが登場しています。もちろん、そういう新たな知識は必要ですが、それらが登場した背景などは理解しておく必要があるでしょう。例えばそれが、メモリーが潤沢な環境を前提としていたとしたら、少なくとも、組込機器向けのWebアプリケーション開発では、ベストな解とはいえません。
カッコ悪いかもしれませんが、レガシーな手法も、こういった環境では役に立つのです。レガシーな手法は、メモリーが少ない時代に考えられたものも多く、今なお組込デバイスでは有効なのです。
今後、Web技術は、PCやスマートフォンを超えてさまざまなデバイスに浸透してくことでしょう。また、スマートフォンは、日本ではハイエンド機ばかりで、年々、そのスペックは劇的に向上しています。しかし、一方で、新興国ではローエンドで安価なスマートフォンが主流です。海外にサービスを広げる際には、こういったローエンドのスマートフォンを無視することはできません。まるで時代を逆行しているかのように見えますが、今後は、ますますローエンドな環境でのWebアプリケーション開発が求められるようになるでしょう。もし、そういう状況になったときに、この記事が役に立てれば幸いです。