2013/07/22更新

[パフォーマンス] CSSとJSをLocalStorageにキャッシュして、2回目以降の表示をちょっとだけ速くしてみる

このエントリーをはてなブックマークに追加            

こんにちは、@yoheiMuneです。

スマホで回線状況が悪い場合(3Gとか使う場合)に、表示遅いなぁーと思うこと多くないですか?

今回は、スマホなどの回線が良くない端末で、Webの表示をより速くする方法として、CSSとJSのローカルストレージキャッシュで高速化する方法をブログに書きたいと思います。

画像



CSSやJSは、expireヘッダが付いてキャッシュされてても初期表示の時間を遅延させる?

iPhoneなどのスマホを使っていると、CSSやJSのキャッシュ機能を有効にしても、初期表示までに時間がかかってしまうことがあります。ブラウザがキャッシュを勝手にFIFOのルールなどで消しちゃうとか。
以下の画像は、iPhone5でiOS6でSoftbankの3G回線を使った際に、とあるサイトを開いた時のネットワーク状況です。
画像

見て頂くと分かりますが、表示までの半分の時間はHTMLを読み込む為のレイテンシー時間(サーバーへリクエストして返ってくるまでの時間)ですが、 CSSやJSの通信時間もかかってしまっています。

この通信時間をなくすことで、初期表示を速くできるのではという考えのもと、CSSとJSのローカルストレージキャッシュを行いました。
実装した結論としては、ローカルストレージキャッシュを使うと、HTMLのダウンロードが終わったすぐに、DOMContentLoadedイベントが発生するようになりました。
また数値的に以下のような改善が見られました(手違いで一部画像の404がありますが、CSSやJSのネットワークロードがなくなり、HTMLのダウンロード完了とともにDOMContentLoadedが発生しています)。
画像
・onload時間が、13%削減の平均1.16秒に。(*1)
・DOMContentLoaded発火時間が、49%削減の平均0.63秒に。(*1)
*1 HTMLロードのレイテンシー後の経過時間

PCでネットワーク状況が良ければ、今回の改善策はあまり意味がありませんが、スマホだと有効かもしれません。



CSSやJSをLocalStorageにキャッシュする実装方法

それでは実装方法です。実装内容は大きく分けて、「1:LocalStorageキャッシュの機能実装」と「2:そのキャッシュ機能を使う部分の実装」に分かれます。
まずは、1つ目の実装です。
/**
  CSS、JS、画像をlocalStorageへ保存して、キャッシュがある場合にはそちらを読む機能
*/
(function() {

  // namespace
  window.cacheModule = window.cacheModule || {};

  // alias
  var aCacheModule = window.cacheModule;
  var storage = window.localStorage;

  // LocalStorageがサポートされているかの判定式
  var supportStorage = storage;

  // LocalStorageにキャッシュする際のキーのプレフィックス
  var PREFIX = 'yoheimcache:';

  // バージョン管理
  // このバージョンを返ると、LocalStorageの内容が最新化されます
  var version = '?ver=3.0.3';


  // バージョンが変わっている場合には、キャッシュを削除する。
  (function() {
    if (supportStorage) {
      var ver = storage.getItem('yoheimVersion');
      if (ver !== version) {
        storage.clear();
        storage.setItem('yoheimVersion', version);
      }
    }
  })();



  // localStorage用のキー生成する関数
  var createKey = function(str) {return PREFIX + str + version;}

  // バージョン付きのURLを生成する関数
  var urlWithVersion = function(url) {return url + version;}


  // AjaxでCSSやJSを読み込む処理
  var ajax = function(url, callback) {

    if (window.XMLHttpRequest === undefined) {
      callback({message: 'non support ajax'}, null);
      return;
    }

    var xmlhttp = new XMLHttpRequest();

    xmlhttp.onreadystatechange = function () {

      if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
        callback(null, xmlhttp.responseText);
      
      } else if (xmlhttp.readyState == 4) {
        callback({message: 'network error.'}, null);
      }
    }

    xmlhttp.open("GET", url, true);
    xmlhttp.send();
  };


  /*
    コンテンツのロード
  */
  var 
    ITEM_TYPE_CSS = '1', 
    ITEM_TYPE_JS = '2', 
    itemHtmlMap = {}
  ;
  itemHtmlMap[ITEM_TYPE_CSS] = {
        directTag: '<style>{{:content}}</style>',
        loadTag: '<link rel="stylesheet" href="{{:url}}">',    
  };
  itemHtmlMap[ITEM_TYPE_JS] = {
        loadTag: '<script src="{{:url}}"><\/script>',
  };


  aCacheModule.loadItem = function(type, url) {

    if (supportStorage) {
      var content = storage.getItem(createKey(url));
      if (content) {
        if (type === ITEM_TYPE_JS) {
          var script = document.createElement('script');
          script.type = 'text/javascript';
          script.text = content;
          document.head.appendChild(script);
        } else {
          var tag = itemHtmlMap[type].directTag.replace('{{:content}}', content);
          document.write(tag);
        }
      } else {

        // キャッシュが無ければ、今回はlink/scriptタグとしておく。
        var tag = itemHtmlMap[type].loadTag.replace('{{:url}}', urlWithVersion(url));
        document.write(tag);

        // Ajaxで取得して、LocalStorage保存しておく。
        window.addEventListener('load', function() {
          ajax(urlWithVersion(url), function(error, data) {
            if (error) {
              console.debug('ajax error. cannot load css.');
              var tag = itemHtmlMap[type].loadTag.replace('{{:url}}', urlWithVersion(url));
              document.write(tag);
              return;
            }
            storage.setItem(createKey(url), data);
          });
        });

      }
    
    } else {
      // LocalStorageサポート外の場合には、普通のタグとして出力する。
      var tag = itemHtmlMap[type].loadTag.replace('{{:url}}', urlWithVersion(url));
      document.write(tag);
    }
  };




  /*
    CSSのロード
  */
  aCacheModule.loadCSS = function(linkRef) {
    aCacheModule.loadItem(ITEM_TYPE_CSS, linkRef);
  };


  /*
    JSのロード
  */
  aCacheModule.loadJS = function(src) {
    aCacheModule.loadItem(ITEM_TYPE_JS, src);
  };

})();

ちょっと長い実装ですが、動きとしては以下のような感じです。

  1. 初めてJS/CSSが指定された場合には、linkタグやscriptタグとして出力して、別途Ajaxで取得してLocalStorageへ保存する。
  2. 2回目にJS/CSSが指定された場合には。LocalStorageに保存していた内容を取得して、style/scriptタグとして出力する。

そして「version」という変数でバージョン管理を行っており、version変数が変わると、LocalStorageの内容が最新下されます。



続いて上記のモジュールを使う側の実装です。
使う側は以下のように実装します。
<html>
	<head>
		<script>
			/* 上記の実装内容を外部ファイルではなく、直書きで書きます。 */
		</script>

		<!-- CSSをロードする場合 -->
		<script>window.cacheModule.loadCSS('css/yoheim.css');</script>

		<!-- JSをロードする場合 -->
		 <script>window.cacheModule.loadJS('js/prettify.js');</script>

		<!--  metaタグなど、他に必要なものがあれば。 -->
	</head>

	<body>
		〜以下、省略〜


通常のlinkタグやscriptタグでの読み込みの代わりに、上記の用に「window.cacheModule.xxx」のメソッドを使うことで、CSSやJSをLocalStorageへ保存、LocalStorageからの読み込みが出来るようになります。



最後に

LocalStorageを使ったキャッシュ機能は、作ったばっかりで色々と不安な点があるのですが、 使ってみて経験を積んで、より快適なWeb生活が送れるような実装が出来ればと思います。

最後までご覧頂きましてありがとうございました。





こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。