ふろしき.js

Web + Mobile + UX + Performance Tech

ページの初期描画を高速化させる、yepnope.jsの使い方

f:id:furoshiki0223:20130627225745p:plain

yepnope.jsは、JavaScriptファイルやCSSファイルを遅延して読み込んだり、条件によって読み込む対象ファイルをスイッチさせることで、ページ応答やパフォーマンスを改善するJSライブラリです。

yepnope.jsの公式Webサイトはこちらですが、2013年6月現在はリンク切れを起こしているので、以下のURLから直接ダウンロードして下さい。

https://github.com/SlexAxton/yepnope.js

1. ページの初期表示を高速化

yepnope.jsの最もシンプルな記述は以下です。

yepnope("./js/jQuery.js");

こういう書き方もします。

yepnope({
load : "./js/jQuery.js"
});

ちなみに、CSSを読み込ませることもできます。

yepnope({
load : [ "./js/bootstrap.js", "./css/bootstrap.css" ]
});

Webブラウザは、初期ロード時に、HTMLファイル内に書かれている全ての要素を逐次実行します。このため、script/linkタグで指定されたスクリプトやスタイルプロパティも、HTMLファイル内で発見される都度、逐次コンパイル/パースされることになります。

yepnopeのような遅延ローダを利用すると、HTML読み込みから描画までの間に発生するファイルのロード処理や、コンパイル/パース処理が後回しにされるため、描画を早く行うことができます。大規模のスクリプトファイルを読み込ませる場合には、高い効果を発揮することができます。

2. 環境に合わせて読み込むファイルを最適に選択

yepnope.jsの謳い文句は、"A Conditional Loader For Your Polyfills!"です。つまり、Polyfillを行うのに適したローダと言えます。

近年はWebブラウザが多様化しているので、HTML5などの新しい機能に対応しているかを判断し、対応していない環境では、シミュレートして動作してくれるPolyfillライブラリを読み込まなくてはいけないケースが多いでしょう。

script/linkタグだと、必要・不必要関係なくロードをしてしまいますが、yepnope.jsを使うと、不要な場合にはロードしないといった対処が行えます。以下がその例です

yepnope({
    test : Modernizr.canvas,
    yep : "./css/main-canvas.css",
    nope : [ "./css/main-nocanvas.css", "./js/excanvas.js" ],
    load : "./js/jQuery.js"
});

この例では、IE8のようなCanvasが実装されていないWebブラウザでも、ある程度はCanvasの動きをシミュレートしてくれるよう、excanvas.jsと呼ばれるPolyfillライブラリを活用しています。

testプロパティには、boolean型を渡すようにします。評価式でも大丈夫ですが、例ではModernizrと呼ばれるWebブラウザの機能実装を確認するJSライブラリの評価値を引数として渡しています。Modernizr.canvasは、Webブラウザにcanvasが実装されている場合はtrueを保持します。

testへtrueが渡された時、つまりcanvasが実装されたWebブラウザである場合、yepnopeはyepプロパティで指定された"./css/main-canvas.css"をロードします。逆にcanvasが実装されいないWebブラウザの場合は、nopeで指定された"./css/main-nocanvas.css"と"./.js/excanvas.js"がロードされます。

これまでは条件付き書式などを使って無駄なロードを回避するのが定石で、もはやおまじないと化していました。canvasを利用する環境では、以下のような記述をよく目にしたのではないでしょうか。

<!--[if IE]>
    <script src="js/excanvas.js" type="text/javascript" charset="utf-8"></script>
<![endif]-->

Webブラウザを限定して機能を指定するというこの手法は、現代においてはバッドプラクティスです。

yepnope.jsにより、実装機能からロードの必要性を判断できるようになり、理想的な方法で機能をスイッチすることができます。先ほどのサンプルでも、canvasの有無に応じて、Webページのデザインを補正するCSSデザインも上手く分岐されており、実装機能に応じた機能の縮退等の対処がうまく行えています。

3. 注意点

yepnope.jsは、JavaScriptCSS、どちらの形式のファイルもロードできます。それ故に、使い方には注意が必要です。また観点によっては、パフォーマンスは劣化しているという判断がされることがあります。これらの注意点について解説します。

3-1. CSSのFOUCへの配慮が必要

CSSファイルは、画面のデザインが遅延して反映されることにより表示が乱れるFOUCと呼ばれる現象を防ぐために、head要素内で読み込むのがお作法です。対してJavaScriptファイルは、レンダリングを出来る限り早いタイミングで完了させてからコンパイルや実行をさせるため、body要素の末端に配置するのがお作法です。

以下のイメージです。

<!DOCTYPE html>
<html>
  <head>
    ・・・
    <link rel="stylesheet" href="css/bootstrap.min.css" />
  </head>
  <body>
    ・・・
    <script src="js/jquery-1.9.1.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
  </body>
</html>

yepnope.jsを使ってロードするとどういう動きになるでしょうか。サンプルと同じく、bootstrapとjQueryをロードさせてみましょう。

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
  </head>
  <body>
    <div>dummy</div>
    <script src="./js/yepnope.1.5.4-min.js"></script>
    <script>
yepnope([
    "js/jquery-1.9.1.min.js",
    "js/bootstrap.min.js",
    "css/bootstrap.min.css"
]);
    </script>
  </body>
</html>

このサンプルは実装の観点から見て、どのような形でロードされるのでしょうか。chromeを使って要素を検証してみましょう。

f:id:furoshiki0223:20130628001910p:plain

yepnopeで指定されたファイルは、最終的にはJavaScriptCSSをロードするための要素に置き換えられ挿入されました。しかしパッと見た印象としては、とても残念な結果に見えます。CSSがbodyの末端へ挿入されているのです。

これをなんとか改善できないでしょうか?head要素上でyepnopeを実行してみましょう。

f:id:furoshiki0223:20130628002405p:plain

全てヘッダへ詰め込まれました。今度はJavaScriptファイルがヘッダに挿入されてしまってます。一見すると、CSSの問題についてはこれで解決されているように見えますが、問題の根本は何も解決されていません。

以下のコードを実行してみましょう。

▼index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <script src="./js/yepnope.1.5.4-min.js"></script>
    <script>
yepnope("css/user.css");
    </script>
  </head>
  <body>
    <div class="dummy">dummy</div>
    <script src="js/load1000ms.js"></script>
  </body>
</html>

css/user.css

.dummy {
    color : red;
}

▼js/load1000ms.js

(function() {
    var begin = (new Date()).getTime();
    while ( ((new Date()).getTime() - begin) < 1000 )  {
    }
})();

"css/user.css"内には、dummyの文字色は赤という指定がされています。"js/load1000ms.js"は、負荷が高い処理を再現するために作成したもので、1000ms(1秒間)のビジーウェイトが発生します。"index.html"からは、scriptタグを使って"js/load1000ms.js"を直でロードしつつ、yepnope.jsを用いて"./css/user.css"をロードしています。

このコードを実行すると、画面にdummyの文字が黒く表示され、1秒後に赤い文字に変化します。これはIEでもChromeでもFirefoxでも同様です。どのような環境でも、FOUCが発生することになります。

yepnopeで指定した"css/user.css"は、"js/load1000ms.js"により発生した、ビジーウェイトの後で実行されいます。CSSが重い処理を行うJSよりも後に読み込まれたため、デザインが反映される前の状態が画面に表示されてしまったのです。HTMLファイルでの見かけ上は、head要素にlink要素が埋め込まれているため、CSSの方が早くロードされているように見えますが、実体としては全く別の動きをしていることになります。

yepnope.jsは恐らく、body要素の末端で実行することが主になるでしょう。polyfillを必要とするシーンや、パフォーマンス向上が求められるシーンで活用されることになるかと思います。しかし、全てのファイルを遅延実行させることに固執してはいけません。CSSFOUCが発生しユーザーエクスペリエンスを低下させることになるため、必要最低限に留めるべきでしょう。多くの場合、JavaScriptのロードの最適化に役立てることができる技と捉えるべきです。

3-2. 常にパフォーマンス向上が見込めるわけではない

yepnopeによるロードは、HTMLファイルの内容を読み込み、一旦表示が完了してから実行されるというイメージなのは理解できたかと思います。実際にどのような手順でロードを行なっているか、chromeのタイムライン機能を使って確認してみましょう。

まずは、yepnopeを利用しないでロードした時です。bootstrapとjQueryを利用したページの例です。実行の遅延を再現するため、実行に100msを要する"load100ms.js"と、200msを要する"load200ms.js"を作ってみました。

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <link rel="stylesheet" href="css/bootstrap.min.css" />
  </head>
  <body>
    <script src="js/load100ms.js"></script>
    <script src="js/load200ms.js"></script>
    <script src="js/jquery-1.9.1.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
  </body>
</html>

f:id:furoshiki0223:20130628012128p:plain

ロードしてからペイントまでに、合計約350ms程度かかりました。ペイントとは、Webブラウザのウィンドウ内に描画を行う処理のことです。今度はこれを、yepnopeを使って表示してみます。

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <link rel="stylesheet" href="css/bootstrap.min.css" />
  </head>
  <body>
  	<script src="js/yepnope.1.5.4-min.js"></script>
  	<script>
yepnope([
    "js/load100ms.js",
    "js/load200ms.js",
    "js/jquery-1.9.1.min.js",
    "js/bootstrap.min.js",
]);
    </script>
  </body>
</html>

f:id:furoshiki0223:20130628012741p:plain

今回は、ペイントが計2回走りました。

一回目は60msでペイント。前回が350msだったので、非常に高速に動作したように思えます。しかし、2回目は開始後から450ms弱程度でペイントされました。トータルで見れば遅くなっています。

遅延ローダはよく"高速だ!"、"パフォーマンスが上がる!"と言われますが、観点次第では遅くなったとも判断できます。そもそも、ローダ自身を読み込み実行する手間が増えるわけですから、トータルが長くなるのは当然なのです。

yepnope.jsなどの遅延ローダが高速と言われるのは、ペイント処理のタイミングが原因です。ページアクセスを開始してから描画されるまでの時間が短いほど、ユーザエクスペリエンスは向上します。

遅延ローダはパフォーマンスを上げるのでなく、体感速度を上げるものです。ユーザを騙しているわけです。より具体的に言うなら、画面ロード直後に起こる重い処理をスキップさせ、ペイント処理を先に発生させているわけです。スキップしているわけですから、そのツケはどこかで払う必要があります。

この微妙なギャップは、業務システムの場合、TAT(ターンアラウンドタイム)の基準によって明確な違いとして現れます。SLA契約の内容が"画面を表示するまで"か"データを出力しきるまで"かで、アプローチが違ってくるのです。

業務システムだと後者を妥当と考える人も多いでしょう。その場合は、遅延実行はあまり有効な策とは言えません。ただ、ユーザエクスペリエンスで見れば、前者の"画面を表示するまで"が重要になるはずです。UXは最近しきりに叫ばれ、業務システムにもその波が押し寄せてきていますが、時と場合によっては、こうしたジレンマがついて回ります。

polyfillの場合は、条件によってファイルのロードを行わないといった用途で活用するでしょう。ファイル数が減るのだから早いのは当然!と思うのは早計です。yepnope.jsは非常に小さなライブラリですが、読み込みとコンパイルが必要なことに変わりはありません。yepnope.jsロードによるオーバヘッド、顧客との非機能要求に対する契約内容(SLA)、両者に正確な認識があってこそ、初めて十分な効果を期待できるでしょう。

4. yepnope.jsの機能仕様

yepnopeは非常にシンプルなライブラリで、仕様はこれだけです。

yepnope([{
  test : /* boolean(ish) - 条件式          */,
  yep  : /* array (of strings) | string */,
  nope : /* array (of strings) | string  */,
  both : /* array (of strings) | string  */,
  load : /* array (of strings) | string   */,
  callback : /* function ( testResult, key ) | object { key : fn } */,
  complete : /* function */
}, ... ]);
  • test : 条件式
  • yep : testがtrueだった場合のみロードするファイル
  • nope : testがfalseだった場合のみロードするファイル
  • both : testがtrue/false関係なくロードされるファイル
  • load : bothと同様の動作をする
  • callback : 個々のファイルのロードが完了した際にコールバックされる関数
  • complete : 全てが完了した際にコールバックされる関数

JavaScriptライブラリなので、この処理をbodyの子要素のうち、一番後ろに配置するのが理想です。

5. 最後に

polyfillの思想の部分と、実態のパフォーマンスの部分、双方からyepnope.jsを評価してみました。この記事を通じて、だいたいどんなイメージで使えば良いかは理解できたかと思います。CSSのFOUCにせよ、遅延実行のオーバヘッドにせよ、何にでもデメリットがあるので、やり過ぎは良くなく、ライブラリが意図する方法で使うのがベストと言えます。

Polyfillライブラリの読み込みに強いうのを強くアピールしてきましたが、Modernizrのダウンロードページにもチェックボックス一つでyepnope.jsを追加できるようなオプション機能が付いていたりしてて、Modernizr側もセットで活用する価値が高いことを正式に認めているわけです。

yapnope.jsは単体でも有用で、単純な遅延ローダとして使うのも良いでしょう。ただ、せっかくyepnope.jsを選択して使うのわけですから、Modernizrへ組み込んで利用してしまうのが良いでしょう。既に様々な用途で使われているようですが、yapnope.jsの良さを十分に発揮させるのは、Polyfillだと私は思っています。