「Webサイト・アプリ高速化テクニック徹底解説」第8回は、モバイルブラウザに向けた最適化について紹介します。 モバイル端末はPCに比べ、CPUやネットワークなどの性能面で劣ることからボトルネックの影響が出やすく、またゲーム開発など突き詰めたチューニングを行う場面では、特殊なノウハウも必要になります。
しかしきちんと最適化を行えば、その効果もその分著しく、比較的低スペックな端末や3G回線であっても、サクサク軽量なサービス提供が可能です。今回の記事では、その勘所をお伝えしたいと思います。
1. ボトルネックを取り除く
まずはやってしまいがちなボトルネックの事例について、挙げていきたいと思います。前述のようにモバイル端末では、その性能からPCよりも顕著に、未最適化箇所が体感に影響を及ぼします。
仕事がらそこそこの数のアプリケーションを見てきましたが、モバイル向けに特化したチューニングができていないのではなく、一般に求められる最適化がなされておらず、それがボトルネックとなって遅いといった事例をよく目にします。
以下では、特にモバイルで問題となる一般的な最適化項目を挙げてみました。個々の詳細な解説については、本連載シリーズの中でより詳細にレポートしていきます。ぜひ、目を通してみてください。
よくありがちな例としては、
- JavaScriptファイルの取り扱いが最適化されておらず、不要なブロッキングが発生している
- 適切なレスポンスヘッダが設定されておらず、ブラウザキャッシュが機能していない(もしくはConditional GETのリクエストを飛ばしている)
- 過剰な数のHTTPリクエストが発生している
といったものがあります。
JavaScriptファイルの取り扱いに注意する
まず影響が大きいのは、JavaScriptファイルの取り扱いです。
script要素はブラウザの他の処理をブロックします。HTML文中にscript要素があると、ブラウザはパース処理など他の仕事を停止し、script要素の評価を優先して実行します。
普段は様々な仕事が並列で走っているのですが、script要素だけは特別扱いで、これはブラウザにとって最も重い仕事のひとつとなります。もちろん、モバイル端末においても顕著に影響を与えます。
script要素の数を減らすか、非同期化しましょう。
defer、async属性を用いることで、上記のブロッキングがなくなります。
非同期化を行わない(行えない)場合はブロッキングの影響を最小限に抑えるよう配慮します。
具体的にはファイル結合を行い、script要素の数を減らし、またbody要素の最下部に置くなど、ブラウザの処理をできるだけ阻害しないようにします
ブラウザキャッシュを有効に機能させる
また、ブラウザキャッシュも重要です。
画像、CSSファイルなどの全アセットがキャッシュに入り、DOMContentLoadedまでにHTTPリクエストが全く発生しない状況では体感速度が大幅に向上します。サクッとファーストビューが表示されるなど、明らかに体感が変わります。
「HTTPリクエストが全く発生しない」ことが重要です。Conditional GET(304)を見かけた場合もきっちり摘み取りましょう。
またサーバ側で画像合成を行うなど、動的なアセットを持つ場合にレスポンスヘッダの付け忘れが起こりやすいようです。 こういった動的なアセットはファーストビューに含まれることも多く、キャッシュの可否で体感速度が変わってきます。 反対に毎回動的に生成するようなものは、もしファーストビューなど重要な箇所に含まれないようなものは遅延ロードに回してやるのもよいでしょう。
例: cache validator(この場合はLast-Modified)が設定されているので正しくキャッシュされる。
例: cache validator に相当するヘッダがなく、キャッシュ不可として扱われる。
HTTPのリクエスト数を削減する
モバイル端末では、PCよりも顕著にHTTPのリクエスト数が性能面に現れます。
理想はファーストビューの表示までに20本未満程度だと思います。
30本以上のリクエスト数を見ると、多いなと感じます。これらにはリクエスト本数を削減するにはスプライトシートを使う、base64 encodeで文字列化して埋め込む、遅延ロードを行う等々、その時々に応じた方法があります。
上記に挙げたボトルネックはどれもGoogle Page Speed等のツールを用いたチェックが可能です。
特殊なノウハウではなく、検出もしやすく改善もしやすいので、ぜひ活用&対応してみましょう。
2. チューニングの為の知識と準備
大まかな流れを掴む
JavaScriptヘビーなアプリケーションなど通常の最適化だけでは十分に軽量化できないものがあります。
そのような場合は、より突っ込んだ測定と最適化のチューニング作業が必要です。
モバイルでのチューニング作業は、ざっくり例えると買い物に近い感覚です。
まずコンテンツの表示にかけてよい時間、つまり予算を定め、DOMContentLoadedまでの所要時間やJavaScriptの処理時間といったパートごとのコスト計測を行い、予算内にそれらが収まるよう、値切ったり(高速化)、予算外に回したり(遅延評価)といったやりくりを行う、といったノリです。
まず予算を決めます。
予算を決めるには、そのコンテンツやアプリケーションがユーザの目から見て使えるようになったと思えるポイント、待ち時間が終わったと感覚的に思えるポイントを定めます。
これはコンテンツによって様々です。ファーストビューが表示されるまでの場合もあれば、メインアプリケーション部分をユーザが操作可能になるまでの場合、アニメーションが再生を開始するまでの場合など、そのアプリケーションの特性によって決まります。
ここまでに要する時間をもって、予算として定めます。
経験則的には、アセットがブラウザキャッシュに収まった状態で、この感覚的待ち時間が大体800ms以下に収まるとサクサクである、読み込みが軽量であると利用者にも感じてもらえることが多いようです。
大体の予算感の参考にしてください。
さて、次に計測を行うのですが、
計測にあたってはターゲット環境の選定が重要になります。
これは端末、OS、ブラウザによって実効性能や動作特徴が変わるので、結果として高速化のために解決するべき問題が環境にひも付いて変わるためです。
ここでは端末、OS、ブラウザ、回線種別の組み合わせを指して「環境」と呼んでいます。特に端末とブラウザの違いについて、下記で見ていきましょう。
ターゲット端末を定める
モバイルで発生する大半の問題は実はCPUやGPU、もしくはネットワーク性能の向上で簡単に解決できてしまいます。
端末性能は日々向上を見せており、今日の課題は明日の課題となり得ないことがあります。
端末の買い換えサイクルは2年程度とよく言われますが、実際にベンチマークを取ると2年前の端末と現在の端末では5~8倍近い性能差が発生します(さらに極端な例もあります)。
本当に解決すべき問題を見定めるためには、その時の状況に即したターゲット端末の選定が重要になります。
本原稿の執筆時点は、低スペック端末が世間から消えるか消えないかのちょうど過渡期です。
シェアを広げつつある性能の良い端末に向けて体験も豊かに補強しつつ、多少年数の経った低スペック端末でも円滑にサービスが動作するような、両方取りのチューニングが求められている時期だと思います。
今回は低スペック端末向けのチューニングも視野に入れましょう。
低スペック端末の代表格としては、例えばiPhone 4があります。iPhone 4はディスプレイサイズに比べてCPU/GPUの性能が十分と言えない、問題を見つけやすいチューニング向きの端末です。
iPhone 4は発売から年数も経過していますが、iOS7がカバーしていることもあり、まだサポートが必要だと思います。
また比較的新しい端末であっても、例えばGalaxy S3など一昨年に発売されたものでも、CPU/GPUの負荷軽減処理が効果的にはたらく低スペック端末に近い挙動のものがあります。
やはりまだ、高速化には端末性能を考慮した作り込みが必要となる、低スペック端末への配慮が必要な時期だと言えるでしょう。
ブラウザ間の差異について
モバイルブラウザは様々な種類が存在しますが、ここではシェアの高いMobile SafariとAndroid Browser、Chrome for Androidといったブラウザに絞って話を進めます。
Mobile SafariとAndroid BrowserはOSのバージョンによっても挙動に違いがあるため、選定時には気をつけてください。
まずブラウザキャッシュの取り扱いは各ブラウザで異なり、体感や最適化に影響を与えます。
Mobile Safariはオンメモリのキャッシュしか持たず、このためOSの都合で頻繁なキャッシュアウトが発生しやすく、また本体やブラウザの再起動など、プロセスの寿命に伴ってキャッシュが破棄されます。
キャッシュ領域のサイズ自体は100MB前後だと言われていますが、状況によってブレがあり、確定した値を持っていません。
ひとつのサイトを訪れ継続して操作を行う場合はキャッシュ効果を期待できますが、サイトを再訪した時など、時間を置いて訪れた際は、以前のキャッシュはもうないものと思った方がよいでしょう。
Android 2.2や2.3といった2系のAndroid Browserでは、キャッシュ領域が8MBとそもそも小さく、キャッシュ効果自体があまり期待できません。
吉報としてはAndroid2系のシェアが下がりつつあることでしょうか。
Android 4系のAndroid BrowserやChrome for Androidになると、85MBのファイルキャッシュ(Persistent Cache)を持つため、比較的良好な効果が期待できます。
max-ageやETagといったキャッシュ寿命の設定も、この辺りから効果を持ち始めます。
このようにモバイル端末では、キャッシュの取り扱いに諸々問題があります。
より詳しい説明が Guy’s Podのエントリに掲載されているため、興味のある方は目を通されるとよいでしょう。
ほかにもHTTPの同時接続数やJavaScriptの実行パフォーマンスなど細かな点が変わります。
例えばiOS5とiOS6では、局所的なJavaScriptの実行パフォーマンスの違いからアニメーション描画に差異があり、体感的な差異をもたらすといったことが実際にありました。
また、iOS7がリリースされたため、既にiOS5の性能劣化はあまり重要な問題とは言えなくなりました。
このように細かなチューニングや性能測定を行う際はOSバージョン、ブラウザによる性能変化に注意し、その時に適切なものを選ぶよう注意が必要です。
計測に際して
計測を行う場合はざっくり、以下の三つのパートから計測を開始することが多いでしょうか。
尚、計測を行う際はMobile SafariのWeb Inspectorなど、結果に影響を与えるデバッガ類を走らせないよう注意が必要です。
- DOMContentLoadedまでの所要時間を計る
- JavaScriptのメインルーチンが呼び出されるまでの所要時間を計る
- 感覚的待ち時間に該当する関数の実行完了までの所要時間を計る
それぞれの計測区結果を基に、さらに絞り込み問題の検出と改善を進めます。
上記それぞれの所要時間に応じた具体的なチューニング方法を次に解説します。
3. DOMContentLoadedまでを高速化する
意外と重くなりがちなのが、DOMContentLoadedまでの所要時間です。
経験則ですが、divやul、liなどHTMLタグの入れ子を深めると、その部分の処理が重くなる傾向があります。
疑わしい部分はコメントアウトして計測してみる、といった方法で特定を行います。
特に低スペック端末が影響を受けやすく、複雑なHTMLで組まれたメニュー部分を排除すると80ms近くDOMContentLoadedが改善した例もあります。
対応としてはHTMLの記述を見直すか、対象箇所を遅延評価にくくり出すのがよいでしょう。 遅延評価の方法はいくつかあり、XHRで取得する、または<script type=”text/htmlfragment”> のような評価されないscript要素で囲み、後でinnerHTMLに放り込むといった方法があります(文字列をHTMLとして評価する場合は、XSSなどセキュリティ面に十分気をつけてください)。
このようにファーストプライオリティに含まれないもの、例えばファーストビューに無関係な画像やHTMLブロックを遅延評価に追い出す方法は効果的でよく用いられます。
例えば、画像の遅延ロードがあります。
img要素のsrc属性を一旦data-src属性に記述しておき、優先すべき処理が終わってから、JavaScriptでdata-srcを拾って画像を改めて読み込むといったことを行います。
重要でない仕事を後ろにずらすことで端末はその貴重なCPU、ネットワークといったリソースを重要な仕事に集中して利用することができます。
このような遅延評価の手法、考え方はモバイルでは有効でチューニング作業全般においてよく登場します。
4. メインルーチン実行までを高速化する
この場合はJavaScriptの読み込みにボトルネックが残っているか、読み込まれたJavaScriptの初期実行で時間を食っているケースを疑います。
各種JavaScriptライブラリも遅延要因になることがあります。
これを計測するには、大変ベタな方法ですが、script要素内にライブラリのJavaScriptコードを貼り付け、前後でDate.nowを取ると計測できます。
var t = Date.now();
// ここにライブラリなど、評価したいJavaScriptコードを挿入する
// (function() { .... })
alert(Date.now() - t);
比較的重めのライブラリの例としてはjQueryがあります。
jQueryはiPhone 4で80ms前後、Galaxy S3で100-200msの時間を初期実行に要します。
これはファイルのダウンロードではなく上記のように純粋にJavaScriptの評価実行に基づいて発生する時間です。
(jQuery1.8.3カスタムビルドで最小時にiPhone4+iOS6.1で54-61ms、フルビルドで80ms前後、2.0.0b1で同じく80ms前後)
仮に手持ちの予算が800msであるとすると、この所要時間は安くない買い物になります。
またアプリケーションが依存するライブラリのコストは、遅延評価やチューニングといった緩和手段が取れず、必ず払うコストになります。
jQueryを例に挙げましたが、この場合はZepto.jsやtt.jsといったより軽量なライブラリに乗り換える手段があります。
もしAndroidとiOSに絞れるコンテンツであれば、ライブラリを使わないのもよいでしょう。
特にゲームやアニメーションを主体としたコンテンツではDOM操作の頻度も減るため、その分のライブラリを削ってゲームエンジンやデータの読み込みに予算を充てることが多くなります。
目的にあったライブラリ選択を行い、予算を節約しましょう。
またよく使うライブラリは、事前にコストを計測しておくとよいでしょう。
他にはCPUやGPUに過剰な負荷がかかっている、といった場合が考えられます。
特にDOMContentLoaded前後の時間帯は計算資源の空きが少なく、過剰なJIT処理の発生や重いアニメーションの再生を重ねると全体が遅延する、描画が一瞬固まるといったことなどがあります。
HTMLの読み込み直後はできるだけCPU、GPUに優しく、負荷の高い処理は遅延させると体感が高速化することがあります。
5. メインルーチン実行自体を高速化する
メインルーチンの実行自体に時間がかかる場合は、フレームワークやコンテンツ固有の特徴にひも付く問題が多くなります。
フレームワークやゲームエンジン個々の話になると、その利用者に限られた内容となってしまうため、ここでは割愛します。
ただ普段自分がよく使うライブラリ、フレームワークがモバイル端末できちんと動作するか実行性能を知り、できれば経験を重ねてチューニングに慣れておくと楽になります。
プロファイラのような各種ツール群もこの辺りから活躍が始まります。
ゲームのように大きなデータの扱いやタイミング制御の伴う画像等のアセットを扱う場合は、並列化、先読み処理といったテクニックもよく使われます。
例えば先読みであれば次のように行います。
function preload() {
// 先読みを行う
loadImage('path');
}
function playScene() { // 画像を必要とする処理を行う loadImage('path', function() { play(); }); }
function loadImage(src, onSuccess, onError) { var img = new Image(); if (onSuccess) { img.onload = onSuccess; } if (onError) { img.onerror = onError; } img.src = src; return img; }
並列化や先読みを行う場合、また少し複雑なアプリケーションになると非同期制御の問題がついて回ります。
筆者もいままで様々な非同期制御ライブラリを使ってきましたが、現在はuupaa氏作のFlow.js(紹介記事)に落ち着いています。
Flow.js の長所としては、単純シンプルがゆえに動作が軽量であることが挙げられ、特に速度面を求める場合は Flow.js は良い選択肢となるでしょう。
例えば Flow.js を使って複数画像の読み込み制御を行う場合は、次のようになります。
function playScene() {
// 複数画像を必要とする処理を行う
var paths = ['path-a', 'path-b', 'path-c'];
var flow = new Flow(paths.length, function() {
// 全ての画像の読み込みが終わると実行される
play();
});
// 画像を読み込む
loadImage(paths[0], flow);
loadImage(paths[1], flow);
loadImage(paths[2], flow);
}
function loadImage(src, flow) { var img = new Image(); if (flow) { img.onload = function() { flow.pass(); } } img.src = src; return img; }
また連続した操作部分やサイト全体を、AJAX化するとサクサク感が増します。
ブラウザはページの初期化に際して様々な処理を行っており、新たなページに遷移するだけで相応のCPU負荷がかかります。
AJAX化することでこれを避けて、CPUに空きを作ってやることができ、ゲームなどの重いアニメーション、CPUを酷使するような仕事をファーストビュー領域内で行う場合などに有効です。
6. JavaScript全体の実行速度を向上させる
カリカリにチューニングを施す場面では、JavaScriptコード自体の最適化が求められます。
例えば三項演算子を使い、cond ? A : B と書くより (cond && A) || B と書いた方が高速です(ただしA≒false があり得るときに注意)。
配列に要素追加を行うのであれば、arary.push(value)ではなくarray[array.length] = valueと書いたほうが速い、オブジェクトのプロパティに2回以上アクセスする場合は変数に代入した方が速いなど、このようなTIPSがJavaScriptには数多く存在します。
塵も積もれば何とやらで、CPUリソースが貴重なモバイル端末、特にゲームやミドルウェアなどの突き詰めた性能が必要となる場面はこのようなJavaScriptコードの最適化が効果を発揮します。
これを行うにはJSXのようなJavaScriptの実効性能の最適化を特徴としたツールの導入が効果的です。
上記TIPS程度であれば人手でまかなえますが、
「関数の呼び出し実行自体が遅く、関数をインライン展開した方が高速」などプログラマの手で生産的にまかなえる範囲を超えたノウハウも存在し、生産的にコードを書きたいのであればアプローチを変える必要があります。
またJavaScriptエンジンが変わるとパフォーマンスの現れ方も変わるため、実行速度最適化の観点から考えるとプログラマが直接JavaScriptを書くのでなく、optimizerやaltJSなどを通じて最適化をかけることが望ましいといった結論に達します。
JavaScriptアプリケーション自体も複雑度が増してきており、本格的なアプリケーション開発においてエンジニアが直接JavaScriptを書く時代も終わりつつあるのかも知れません。
そろそろaltJSの時代だな、と思います。
最後に
以上、ざっくりとモバイルでの最適化手法をお伝えしてきました。
上手に最適化を施してやれば、通常のWebページ、Webアプリケーションはもとより、ゲームやアニメーションといった重量級のコンテンツでも、十分にサクサク快適に動作します。
そのために計測を行い、どこにコストを費やしているのか、問題を正確に把握するアプローチが大切です。
「重い」と言われた場合、軽量化を求められた場合はどの端末で、どういった操作を行った時に重いのか、改善を必要としているのか、まず環境の特定から入り測定を行いましょう。
時々「ページの体感が重いから、画像を含むアセット量を500KB以下に定める」といったアプローチも聞きますが、それでは問題の一部しか解決できず、サクサクな体感に至らないケースがままあります。
HTMLの読み込みコスト、JavaScriptによるブロッキング、フレームワークやゲームエンジンに紐付いた実行遅延、、CPUやネットワーク性能に限りがあるモバイル環境下では様々な要因が絡んで「もっさり」が発生します。
サクサク化への効果的なアプローチと問題解決に、本稿がお役に立てることを願っています。