モバイルアプリを

"浅く"チューニング

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で必要なカラムのみ指定すると、リソースを節約

できる。

今回行ったチューニングの手順

  1. 機能の実装と高速化は分けて行った(手が遅くなるため)
  2. トータルのレスポンス速度をログから測定
  3. 遅い原因をログから探る
  4. 重いjsライブラリはCDNから読み込む
  5. インデックスが適切に張られているか調査する
  6. 同じリソースに対してSELECT文が繰り返されていれば、includesメソッドを使うべきか検討する
  7. クエリ結果のキャッシュ