モバイルアプリを
"浅く"チューニング
2014.10.28 社内勉強会
アジェンダ
- 推測するな、計測せよ
- フロントエンド高速化
- サーバサイド高速化
推測するな、計測せよ
- チューニングに終わりはない
- ボトルネックになっているものから対処する
- チューニング終了の基準を決めておく
どこまで高速化するべきか?
- ロード速度: 1-2sec => 2secを超えると遅いと感じる
- ページサイズ: 1MB以下(できれば500KB以下)
※ページのスタイルによる
※どうしてもサイズを減らせない時は遅延読み込み等を検討
計測ツールは色々ある
- Safari Web Inspector
- Chrome developer tools
- Railsのログ
- 外部サービス
- explain
- speedlimit
- 自作計測ツール
Safari Web Inspector
Text
- 実機でも、シミュレータでも計測できる
- サイズの大きいJavaScript, CSS, imageを特定する
- 計測時に、キャッシュはオフにする
Railsのログ
- Completedは全体の処理時間
- Renderingはテンプレート等のレンダリング処理
- DBはActiveRecord関連の処理
※ クックパッドはサーバサイドの応答速度200msを
保つようにつとめている
Completed in 1.08156 (0 reqs/sec) |
Rendering: 0.25919 (23%) | DB: 0.78924 (72%) |
200 OK [http://foobar.com/entries/show/64343]
Google PageSpeed Insights
- URLを入力すると、サイトの改善ポイントを教えてくれる
explain
Text
- SQLの実行計画を確認する
- 致命的なクエリはtypeフィールドを見ればわかる
- インデックスが使用されているか確認する
Speed Limit
- 低速回線での利用状況を再現するツール
自作ツール
- Author: Nさん
- まずはレスポンスの遅いURLを確認する
- チューニングの一次調査に
※非公開
フロントエンドの高速化
- 応答速度の向上と負荷分散
- HTTPリクエストを減らす
応答速度の向上と負荷分散
- 大きめのライブラリ(jQuery)やCSSはCDN(cdnjs.com)を用いて配信
- 静的ファイルへのアクセスを分散させる
- 最寄りのキャッシュサーバから配信するため高速
CDNからjsをロード
- CDNがあぼーんした時の例外処理を記述する
- あぼーんした時はローカルファイルを読み込む
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
(window.jQuery || document.write('<script src="/js/jquery.js"><\/script>'));
</script>
HTTPリクエストを減らす
- あらかじめNativeに配置した画像をWebViewで呼び出す
- NSURLProtocolのsubclassを用いる
<html>
<body>
<h1>we are loading a custom protocl</h1>
<b>image?</b><br/>
<img src="myapp://image1.png" />
<body>
</html>
http://stackoverflow.com/questions/5572258/ios-webview-remote-html-with-local-image-files
サーバサイドの高速化
- 無駄なSQL発行を減らす
- インデックスを使う <= ここは省略
- kvsでキャッシュ
無駄なSQL発行を減らす
- スローログに乗らない細かいSQLも負荷になる
- Eager Loading
- ActiveRecordが生成するSQLを知る
ありがちなパターン
- いわゆるN+1件問題
- 油断すると、すぐにこうなる
Tweet.limit(10).each do |t|
p "#{t.user} posted #{t.content}"
end
Tweet Load(9.8ms) SELECT `tweets`.* FROM `tweets`
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE
Eager Loadingを利用する
- includesで関連するモデルを先読みする
- クエリが劇的に減る
Tweet.includes(:user).limit(10).each do |t|
p "#{t.user} posted #{t.content}"
end
Tweet Load(9.8ms) SELECT `tweets`.* FROM `tweets`
User Load(5.2ms) SELECT `users`.* FROM `users` WHERE `users`.id
IN (724,1402, 2277,3254,3696,5518,4810,8896,98,9999)
ActiveRecordが生成する
SQLを知る
Tweet.uniq.to_sql
=> SELECT DISTINCT `tweets`.* FROM `tweets`
ActiveRecordが生成する
SQLの実行計画を知る
Tweet.uniq.explain
=>EXPLAIN for: SELECT DISTINCT `tweets`.* FROM `tweets`
kvsでキャッシュ
- キャッシュストアを何にするか
- クエリ結果のキャッシュ
キャッシュストアを何にするか
- Memcachedを使うかファイルキャッシュを使うか
- アプリケーションサーバが1つであれば、ファイルキャッシュの方が速度が出るかもしれない。
- アプリケーションサーバが複数の場合、memcachedだと複数のサーバから共通のキャッシュを使うことになり、キャッシュヒット率が高くなる。
クエリ結果をmemcachedに格納する
posts = Rails.cache.fetch('unique_post', expires_in: 10.minutes) {
Post.get_uniques.select('id, user_id')
}
Railsがunique_postというkeyでmemcachedに問い合わせて、objectが存在しなければcacheにクエリ結果(object)を
格納する。
selectで必要なカラムのみ指定すると、リソースを節約
できる。
今回行ったチューニングの手順
- 機能の実装と高速化は分けて行った(手が遅くなるため)
- トータルのレスポンス速度をログから測定
- 遅い原因をログから探る
- 重いjsライブラリはCDNから読み込む
- インデックスが適切に張られているか調査する
- 同じリソースに対してSELECT文が繰り返されていれば、includesメソッドを使うべきか検討する
- クエリ結果のキャッシュ