これは AngularJS Advent Calendar 2014 の5日目(12/5)の投稿です。
最初は AngularJS に絵文字を導入する方法とか、チュートリアルを組み込む方法とか、AngularJS での Chromeアプリの開発など、過去に書いたものの中から「変わりダネ」を整理して再投稿しようと思ってたのですが(もし興味があれば過去のをご覧ください)、
絵文字 ⇒ AngularJSで絵文字を表示するメモ
チュートリアル ⇒ AngularJSアプリケーションにチュートリアルを簡単に導入する
Chrome ⇒ ChromeアプリをAngularJSで書くまでの手順
今回はアドベントカレンダーらしく? 読む人にとって役に立ちそうなものとして、モバイルフロントエンドでの通信量/通信数/通信頻度の削減ついて書きます。
これを書こうと思った動機としては、チューニングするのに必要不可欠であるものの、あまり議論というかネット上にまとまった情報が無かった為です。
大した内容では無いのですが、サーバ側を経験していないフロントエンドエンジニアにとっては役に立つかな、と思い..
あと想定するアプリケーションとしては、タイトルにあるように「モバイル向け」かつ SPA(シングルページアプリケーション)とします。
また、この投稿では検証用のアプリケーションの準備から書いてますが、まとめだけご覧になりたい場合は後半だけ参照ください^^;
1. 検証用のアプリケーションを準備
① AngularJS の雛形を生成
Yeoman の generator-angular
を利用しました。手順については割愛します。(過去に書いたので、よろしければ参照ください。)
ちなみに、ジェネレータを起動する際にオプション --coffee
を付与して、CoffeeScript で書けるようにしてあります。
② AngularUI Router の導入
URLのルーティングに AngularUI Router を導入しました。
導入した理由としては、AngularJS デフォルトの ngRoute($routeProvider)だとテンプレート(
.html
ファイル)を JavaScript側にキャッシュ出来ないからです。
Bower で次のようにインストールし、
$ bower install angular-ui-router --save
後は、こちらの記事(AngularUI Routerのつかいかた)を参考に、アプリケーションに組み込みました。
URLのルーティングは次のようになっています。
angular
.module('cacheApp', [
'ngAnimate',
'ngCookies',
'ngResource',
'ngRoute',
'ngSanitize',
'ngTouch',
'ui.router'
])
.config ($stateProvider, $urlRouterProvider) ->
$stateProvider
.state 'main',
url: '/main'
templateUrl: 'views/main.html'
controller: 'MainCtrl'
.state 'about',
url: '/about'
templateUrl: 'views/about.html'
controller: 'AboutCtrl'
$urlRouterProvider
.otherwise '/main'
③ APIを発行する機能を追加
単純に、画面でボタンが押下されたら、適当なAPIを叩くだけのものです。
本当は、この手の処理は Controller に書くべきでない気がしますが、実験用なので..
angular.module('cacheApp')
.controller 'MainCtrl', ($scope, $http) ->
$scope.call_api = ->
$http(
method: "GET"
url: "dummy.json"
cache: true
).success((data, status, headers, config) ->
return
).error (data, status, headers, config) ->
return
console.log "APIを発行しました。"
return
return
④ 成果物
こんな感じです。
実際に動かせるものをこちら(GitHub Pages)に用意しました。
⇒ http://hkusu.github.io/AngularJS_cache_demo/dist/
利用ライブラリの各バージョンは次のとおりです。
AngularJS:
1.3.5
AngularUI Router:0.2.13
ソースコードはこちら(GitHub)に置いてありますので、必要あらば確認ください。
⇒ https://github.com/hkusu/AngularJS_cache_demo
2. Chrome Dev ツールで確認してみる
トップページ(上記のデモのページ)へアクセスしてみます。次のようにサーバへファイルを問い合わせしています。
ちなみに「ブラウザ」のキャッシュは OFF にしてあります。
[About]ボタンを押下します。SPAの部分HTMLファイル about.html
にアクセスがありました。
[Home]ボタンを押下して、トップページに戻ってみます。ここでは何の HTTPリクエストも発生しません。
AngularUI Router により、部分HTMLファイル
main.html
がメモリへちゃんとキャッシュされているようです。(この仕組みは後述します。)
[API発行]ボタンを押下してみます。dummy.json
へのアクセスがありました。
[API発行]ボタンは、2回目以降は、何度押してもサーバへ問い合わせはしません。(この仕組みは後述します。)
ブラウザのキャッシュを ON にすると、サーバからは HTTP 304
コードが返ってきます。(ブラウザのキャッシュを利用して画面が描写されている。)
3. 通信量/通信数/通信頻度を削減するには
私は元々は(ていうか今でも?)サーバ側のエンジニアなのですが、サーバ側の知識と AngularJS の知識、また今回の検証用アプリケーションを通して得られた知見を元に、こう最適化すればいいんじゃないか、というのをまとめてみました。
① 静的ファイル(.html
.js
.css
画像)の配信
i) ファイルの結合・縮小(.js
.css
)
複数のファイルを1つのファイルを結合(concat)し、余分な改行やスペースを削除して容量を縮小(minify)します。例えば今回の検証用のアプリケーションだと、次のような感じです。
上記の例だと、外部ライブラリ(vendor.
)と自作のもので別れてはいます。なぜ結合した方がいいか?に対する理由としては、ファイルが少ないほど接続コストが下がるからです。
手動でやると死ぬので^^; Grunt や Gulp 等のタスクランナーで自動化するのが一般的かと思います。
ii) 【サーバ側】ブラウザへのキャッシュを指定する
静的ファイルをサーバから送出する際、HTTPのレスポンスヘッダ(Cache-Control: max-age=秒
もしくは Expires: 日付
)でブラウザ側キャッシュの期限を指定します。
(Last-Modified
については、後述の条件付きリクエストで必要なので、このヘッダも付与しておきます。)
この期限内であれば、ブラウザは サーバ側へ問い合わせをしません。 ですので通信コストを節約できます。
HTTPのリクエスト/レスポンスヘッダとブラウザのキャッシュの関係性については以前にこちらに書いたので、よろしければ参照ください。
⇒ 静的リソース(HTML,JS,CSS,画像)のブラウザキャッシュを制御
ただ注意なのが、キャッシュしてしまうと、運用(裏側)でアプリケーションを更新したいのに.. という場合に困ります。ですので、ページの起点となる index.html
に相当するファイルはキャッシュせず(Cache-Control: max-age=0
もしくは Expires: 過去の日付
)とし、その index.html
から読み込む JavaScriptファイル、CSSファイルは、アプリケーションを更新する度にファイル名を変更する、というのが良いかと思います。
(まあ、Grunt や Gulp 等のタスクランナーでやってくれるのですが..)
index.html
以外については、1時間でも1日でもキャッシュさせてしまえば良いかと。
(ただ後述の条件付きリクエストを利用すれば、どのみち通信量は大したことないかも。)
ブラウザによっては、リロードアクションでキャッシュの期限に関係なくサーバへ問い合わせるものもある、気がします。
iii) 【サーバ側】ファイルに変更が無いなら HTTP 304
コードを返す
ブラウザ側にキャッシュがあれば、次のように条件付きリクエスト(If-Modified-Since
ヘッダ)を送ってくるはずなので、
サーバ側では、リクエスト中の日付と、サーバに保存されているファイルの日付を比較して、もし差異が無いならファイルを送出せずに HTTP 304
コードを返却します。
そうすると、ブラウザ側はキャッシュされているファイルを再利用します。つまり通信コストが節約できます。
.. ただ、大概のWEBサーバは自動でやってくれるはず。
最近は
Etag
ってあまり使われない気がする。気のせい? でも確かにEtag
は複数WEBサーバ、CDNの時代の今では運用しにくい気がする。
iv) SPAの部分HTMLファイル(.html
)
AngularJS であれば AngularUI Router を導入することで、そのHTMLファイルが必要になった時点でファイルを取得してくれます。
また、取得したHTMLファイルはブラウザをリロードしない限り、メモリに保持されたものが使いまわされます。
(厳密には $templateCache《結局は$cacheFactory》にデータが格納されます。)
angular
.module('cacheApp', [
〜
'ui.router'
])
.config ($stateProvider, $urlRouterProvider) ->
$stateProvider
.state 'main',
url: '/main'
templateUrl: 'views/main.html'
controller: 'MainCtrl'
.state 'about',
url: '/about'
templateUrl: 'views/about.html'
controller: 'AboutCtrl'
$urlRouterProvider
.otherwise '/main'
v) 【サーバ側】gzip 圧縮して配信
ブラウザが gzip ファイルに対応している場合は、HTTPのリクエストヘッダでその旨を送ってくるはずなので、
その場合はサーバ側で gzip ファイルを配信するようにします。
ただ、ブラウザ側の解凍負荷もあるので注意。
もし WEBサーバが Apache の場合は mod_deflate
を利用すると手軽です。
ちなみに今回、気がついたのですが、GitHub Pages は gzip でファイルを配信してくれるみたいですね。
vi) 【サーバ側】接続の keepalive
どれだけ効果があるかは私には知識が無いのですが、HTTP の接続コストが下がります。1つの画面で大量に静的ファイルを読み込む必要がある場合などに。
(ただ、大概のWEBサーバでは対応されているとは思いますが。)
vii) 画像の最適化
- 画像の縦横サイズ、容量が無駄に大きすぎないか注意します。
- デバイスによって引き伸ばされても綺麗に見えるように、SVG形式の利用を検討します。
- でも Android 2系 だと使えなかったような?
- 可能であれば CSSスプライトを利用して、接続数が増えないようにします。
viii) CDN(Contents Delivery Network)に載せる
AWS でいうと CroudFront
、他には Akamai などあるかと思います。ユーザに物理的に近いエッジから配信できる&負荷を気にしなくて良い、というメリットがあります。
ただ、上記の
【サーバ側】
に書いた対応が出来なくなるかも。
② WEB-APIへの対応
A. 発行する側(JavaScriptアプリケーション)
i) 設計レベルの話
まず設計として、無駄に同じ API を何度も発行しないようにします。
- Model層にデータを格納 ⇒ 使い回す
- シングルトンにする
- シングルトンにしないまでも、通信中に再度、通信が指示されないような仕組み
ただ毎回サーバ側へ問いあわせるべきデータもあるので、データの性質による。
ii) HTTPリクエスト/レスポンスのキャッシュ
AngularJS でいうと $http
オブジェクトで出来ます。次のように cache: true
とすると、ブラウザをリロードしない限り、メモリに保持されたデータが使いまわされます。
(厳密には $cacheFactory にデータが格納されます。)
$http(
method: "GET"
url: "hoge.json"
cache: true
).success((data, status, headers, config) ->
#..
return
).error (data, status, headers, config) ->
#..
return
こちらもデータを使いまわしてよいかはデータの性質による。
B. 発行される側(サーバ側)
もしサーバ側のアプリケーションも案件でコントロールできる範囲なら、上記の「① 静的ファイル」と同じ話です。ブラウザから見ると、WEB-API も静的ファイルも挙動は変わらないので。
③ そのほか可能であれば検討
i) WEB-API を叩く箇所
- リスト系の画面であれば、例えば100件あったら5件づつ持ってくる。
- Deferred/Promise の仕組みを活用して非同期に。画面が固まらないように。
- いま必要ないデータであっても先読みでデータを持ってくる。
ii) 画像ファイルの遅延ロード
ブラウザの表示領域にはいった段階で読み込む。AngularJS で何か良いモジュールはあるのかな?
iii) JavaScriptファイル(.js
)の遅延ロード
例えば RequireJS を利用して、必要な段階になったら読み込む。ただ個人的には、アプリケーションの複雑度はあがるのでちょっと.. という感じです。また、複数ファイルを結合(concat)する恩恵は受けられなくなるかと。
iv) ブラウザのWEBストレージを利用
永続化してよいデータであれば、ここに格納しておけば、サーバに対して API を発行する回数が減るかと。AngularJS だと ngStorage あたりを利用すればいいのかな?
ただ、アプリケーションをバージョンアップしたい場合に、整合性をとる必要があります。バージョン毎に Key の Prefix をつけておくとか?
補足:
HTML5 の Application Cache もいれようと思えばいれれるのですが、あれはオフライン用というか、自分的に使い勝手が悪いので現段階では検討にいれていません。
4. おわりに
後半は書いてて疲れちゃったので荒削りでしたが、最適化について考える機会となれば幸いです^^;