59
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Railsで高レスポンスかつ初期表示も速いスマホ用Webサイトを作る

Last updated at Posted at 2015-01-02

SPAについて

前書き

スマホはネイティブアプリを使う機会が多いため、すばやいレスポンスが当たり前の感覚になっています。
Webサイトでも同じような感覚を実現できるよう、SPAという仕組みで構築することが昨今の流行となっています。
Rails4ではレスポンス向上のためにturbolinksというSPAに近い仕組みが用意されていますが、簡単に実現できるかわりに、パフォーマンス改善も限定的になってしまうため、ここではSPAを取り上げます。

##SPAとは
SPAとはSinglePageApplicationの略で、最初にWebサーバからHTMLを取得した後は、JavaScriptで画面を切り替えていき、JavaScriptライブラリやCSSのLoadingやParseを最初のHTML取得時のみにして、さらにサーバとの通信も最小限とすることで、ネイティブアプリと変わらないような快適なレスポンスを実現する仕組みです。
最近はSPAのフレームワークとしてAngularJSが取り上げられることが多くなりました。

SPAの問題点

SPAを使うことで、ネイティブアプリと遜色ない高レスポンスのWebアプリを構築することができますが、すべての画面の処理を記述したJavaScriptにより、jsファイルが大きくなって、Loading & Parseに時間がかかることにより、初期表示が遅くなってしまいがちです。

特にスマホの場合、PCと比べて回線も遅く、処理能力も落ちるため、より初期表示の遅さが問題になりやすいです。

SPAなんだから、初期表示遅いのは仕方ないよねと諦めてしまいがちなのですが、サイト表示が2秒遅いだけで直帰率は50%増加!の記事にもあるように、折角SPAで快適なページ遷移を実現しても、ページを遷移するまでそのサイトがSPAかどうかは誰もわからないため、初期表示の段階でこのサイト重いと認識されると直帰されてしまいます。

SPAでも初期表示を速くする対応

JavaScriptをできるだけコンパクトになるようにする

下記のようにフレームワークやライブラリをコンパクトなものを選択したり、さらには必要最小限になるよう自分で実装するなど頑張る必要があります。

SPAフレームワーク サイズ
AngularJS 108Kb
Backbone+Underscore 11.7Kb
ライブラリ サイズ
jQuery 82Kb
Zepto(jQuery互換) 25Kb

初期表示時にサーバとの通信を極力行わない

SPAで作成する場合、各モデルごとにサーバとデータを同期する必要がありますが、データを初期のHTMLにJSONデータとして埋め込んでしまうことで、Ajax通信を回避することが可能です。すべてのモデルで行うとサーバ自体の処理が重くなったり、ページのサイズが大きくなってしまうので、初期表示に必要な分だけのデータが望ましいです。

index.html.erb
<script type="text/javascript">
  var initialData = <%= raw(@initialData.to_json) %>
</script>

jsファイルを分割してOn Demand Loadingにする

パフォーマンス改善の対策として、全部の処理をひとつのjsにまとめることでリクエスト数を減らすことが一般的ですが、SPAの場合、全部の画面の処理をjsにまとめてしまうと、機能追加する度にjsファイルが大きくなってサイトが重くなってしまい、せっかく軽いライブラリを使っていてもその効果が薄くなってしまいます。
そのため、最初は必要最低限のjsだけ読み込んで、以降は必要に応じてjsを動的に読み込むことで、jsのサイズを抑え、初期表示を早くすることができます。

Railsの場合はsprocketsを使って簡単にjsのグループ分けすることができます。On Demandのjsのロードは下記のように実装することができます。

js_loader.js
JsLoader = function(srcMap){
 this.srcMap = {};
 for(key in srcMap){
   this.srcMap[key] = {state: "unload",src: srcMap[key],cb: []}
 }
};
JsLoader.prototype = {
  load: function(m,cb){
    var that = this;
    if(!this.srcMap[m]){
      throw m + " was not found";
    }
    if(this.srcMap[m].state == "unload"){
      var fjs = document.getElementsByTagName("script")[0]
      var script = document.createElement("script");
      this.srcMap[m].state = "loading"
      that.srcMap[m].cb.push(cb);
      script.onload = function(){
        that.srcMap[m].state = "loaded"
        while(that.srcMap[m].cb.length > 0){
          that.srcMap[m].cb[0]();
          that.srcMap[m].cb.shift()
        }
      }
      script.onreadystatechange = function(){
        if (script.readyState == "loaded" || script.readyState=="complete"){
          that.srcMap[m].state = "loaded"
          while(that.srcMap[m].cb.length > 0){
            that.srcMap[m].cb[0]();
            that.srcMap[m].cb.shift()
          }
        }
      };
      script.src = this.srcMap[m].src;
      fjs.parentNode.insertBefore(script,fjs);
    }else if(this.srcMap[m].state == "loading"){
      this.srcMap[key].cb.push(cb);
    }else{
      cb()
    }
  }
}

application.js
//sprockets Topページ表示に最低限必要なjs
//= require jquery
//= require ./lib/underscore
//= require ./lib/backbone
//= require ./lib/js_loader.js
//= require_tree ./templates
//= require_tree ./backbone
misc.js
//sprockets それ以外のページ表示に必要なjs
//= require_tree ./misc_templates
//= require_tree ./misc_backbone
sample_controller.rb
class SampleController < ApplicationController
  def index
    @src_map = {"misc" => ActionController::Base.helpers.asset_path("misc.js")}
  end
end
index.html.erb
<html>
<head>
  <meta charset="UTF-8">
  <%= layout_meta_tags %>
  <%= stylesheet_link_tag    "application", media: "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
  <title>Sample</title>
</head>
<body>
  <script type="text/javascript">
   var router = new SampleRouter({jsLoader: new JsLoader(<%= raw(@src_map.to_json) %>)});
  </script>
</body>
</html>
router.js
var SampleRouter = Backbone.Router.extend({
  initialize: function(options){
    this.jsLoader = options.jsLoader
  },
  routes: { 
    "topics": "topics",
    ".*": "top"
  },
  top: function(){
    //描画
  },
  topics: function(){
    this.jsLoader.load("misc",function(){
      //描画
    }.bind(this))
  }
});

#まとめ
私が今、開発、運用している無料のFX情報サイトであるsmartFX(smartfx.jp)には、上記の3つの方法を実践しています。
そこそこ大きいSPAのサイトですが、SPAではないサイトに負けない初期表示の早さが実現できていると思っているので、ぜひ見てみてください。

59
56
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
56

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?