JavaScriptの読み込みにおける非同期スクリプト注入の悪影響

Synchronous(同期)スクリプトは効率が悪い。というのも、ブラウザにDOM構築をさせ、スクリプトを読み込ませ、残りのページをリロードする前に実行してしまいます。今さらな話ですが、これがわれわれプログラマがasynchronous(非同期)スクリプトをよく使うようになった理由です。ここに分かりやすい例があります。

<!-- BAD: blocking external script -->   
<script src="http://somehost.com/awesome-widget.js"></script>

<!-- GOOD: remote script is loaded asynchronously -->   
<script>
    var script = document.createElement('script');   
    script.src = "http://somehost.com/awesome-widget.js";   
    document.getElementsByTagName('head')[0].appendChild(script);   
</script>

2つの違いはなんでしょう? “悪い”例では、DOM構築をブロックし、スクリプトが取得されるのを待ってプログラムを実行します。それから残りのドキュメントの実行が続けられます。一方、2つ目の例は、インラインスクリプトを使用した例です。
インラインスクリプトを実行し、外的リソースを示すスクリプトを作成し、ドキュメントに付け加え、DOMを処理し続けます。

つまり決定的な違いとはScript-injectedはネットワーク上の通信を遮断しないということなんです。

すごいと思いませんか? Script-injectedは優れものです。ただし処理速度はそれほどでもありませんが。

インラインJavaScript ソリューションは繊細ですが、とても重要な(ほぼ見落とされてきましたが)パフォーマンスを達成させます。つまりインラインスクリプトは、プログラムを実行する前に、CSSOMをブロックします。どうしてでしょう? ブラウザはインラインブロックがスクリプトで何を実行しようとしているかに気づきません。そのため、JavaScriptはCSSOMにアクセスでき処理することが可能であり、CSSがダウンロードされ、解析されるまでブロックし、待機します。実際のネットワーク通信は膨大なデータが処理されることが望まれます。次の例を見てみましょう。

上記の図は、ページのトップに配置したCSSファイルとページ下に配置した2つのscript-injected “async scripts”を読み込んでいます。言い換えれば、それぞれが“最高のパフォーマンス”をしていることにつながります。ただしスクリプト自体はCSSOMが準備できるまで実行されません。つまりインラインブロックの動作が遅れ、連動して、ネットワークリクエストが送られるのも遅くなるということです。最終的に、スクリプトが実行されるのは、ページリクエストが開始されてから3.5秒後となります。

注目すべきは、CSSではレスポンスに2秒もの遅れが生じ、JavaScriptでは1秒の遅れが生じたことです。すなわちCCS/CSSOMとJavaScriptの処理に依存しているということです。

今度は“悪い”例と比べてみましょう。ここでは、2つのブロッキングスクリプトタグを使います。

ちょっと待ってください。何が起きていますか?どちらのスクリプトも読み込みが速く、ページリクエストが開始されてから2.7秒ほどで実行されました。特筆すべきは、CSSが利用可能になるまで(2.7秒をマーク)スクリプトはまだ実行されていますが、CSSOMの準備完了までには、すでにスクリプトは読み込まれています。そのため、処理速度が1秒以上、更新されたことになります。ということは、いままで誤った処理を行っていたのでしょうか? その結論を出す前に、もう1つ例を見てみましょう。今度はasync属性で試してみます。

<script src="http://udacity-crp.herokuapp.com/time.js?rtt=1&a" async></script>   
<script src="http://udacity-crp.herokuapp.com/time.js?rtt=1&b" async></script>   

async属性であれば、利用開始とともに、スクリプトは非同期で実行されます。一方でasync属性でなければ…ユーザエージェントがページを解析する前に、スクリプトが読み込まれ、直ちに実行されます。http://www.w3.org/TR/html5/scrpting-1.html#attr-script-async

スクリプトタグのasync属性は、2つの重要なプロパティを持っています。その2つとはブラウザにDOM構築をブロックさせないこと、CSSOM上でのスクリプト実行をブロックしないことです。結果として、スクリプトはダウンロードが(1.6秒ほどで)完了すると、CSSOMを待つことなく実行されます。ここまでの結果をまとめてみましょう。

    スクリプトの実行 読み込み
script-injected ~3.7秒 ~3.7秒
ブロックキング
スクリプト
~2.7秒 ~2.7秒
async属性 ~1.7秒 ~2.7秒

なぜ、長い間、プログラマは、このパターンを使用を主張してきたのでしょうか?

  1. “async”(非同期)はIE 8/9, Android 2.2/2.3.といった、いくつかの前の型のブラウザに対応していません。そのため、以前のブラウザは非同期の属性を無視し、ブロッキングスクリプトと見なします。かつては、大きな問題でした。そして次のポイントにいきつきました。

  2. 最新のブラウザは“プリロードスキャナ”(もちろん、IE 8/9, Android 2.2/2.3でさえもを持ち、ドキュメントパーサーがブロックされると起動し、ドキュメントの中身を検証して、優先度の高いレンダリングパスのブロックが解かれたら、直ちに実行すべきリソースを見つけます。

The script-injectedパターンは、<script async> には有益ではありません。その理由として script-injectedパターンが初めて導入された時、<script async> は利用できず、プリロードスキャナもありませんでした。しかし、その後、script-injectedスクリプトに代わりasync属性を利用しながらも、さらにパフォーマンスの質を上げることが必要となりました。つまり、async scriptsが不要と見なされたのです。

またプリロードスキャナは、スクリプトやリンクタグのsrc/href属性通して指定されたリソースだけを見つけられるということに注意してください。プリロードスキャナはインラインJavaScriptブロックを実行することはできません。つまり、プリロードスキャナではscript-injectedの利点を見つけることができないのです。この結果を受け、最新の解決策となるのが、以下です。

<!-- BAD: the pre async / pre preload scanner era -->   
<script>   
    var script = document.createElement('script');   
    script.src = "http://somehost.com/awesome-widget.js";   
    document.getElementsByTagName('head')[0].appendChild(script);   
</script>

<!-- GOOD: modern, simpler, faster, and better all around  -->   
<script src="http://somehost.com/awesome-widget.js" async></script>   
<script src="..."> <script async src="...">
DOM構築をブロックする DOM構築をブロックしない
CSSOMで実行がブロックされる CSSOMで実行がブロックされない
プリロードスキャナが検出可能 プリロードスキャナが検出可能
スクリプト順に実行 非順序型実行
実行順序が重要な場合に使用し、スクリプトは一番下に置く。 順番がバラバラになる可能性があるので、順不同になっても構わないファイルに適している。

スクリプトを実行する前に・・・

誤解のないように言うと、すべてのインラインJavaScriptを避けるべきだと言っているわけではありません。実際、インラインJavaScriptが正しい解決法である場合もあります。ただ、以下の項目に留意する必要があります。

  1. async属性は実行順序が保証されません:スクリプトが得られた時点で実行され、ドキュメント内の順序や位置は無効になります。依存関係のあるスクリプトの場合、依存関係を解消できますか? または、defer属性付きで実行できますか? あるいは実行順序を無視できますか? 非同期関数のキューパターンを調べてみるといいでしょう。   
  2. 非同期関数のキューパターンでは、変数の初期化(インラインスクリプトブロックが必要になります)が要求されますが、初めからやり直すのでしょうか? そうではありません。CSSの宣言の上にインラインブロックを配置する(ドキュメントのヘッダー部分でCSSより先にJavaScriptを読み込ませる)と、インラインブロックが直ちに実行されます。インラインブロックの問題は、CSSOMでブロックしなければならないことですが、CSSの宣言の前に配置すると、直ちに実行されます。   
  3. では、CSSより先にあるJavaScriptをすべて移動させればいいのでしょうか? そうではありません。ブラウザができるだけ早くCSSを発見し、実際のページ内容の解析を開始できるようにヘッダー部分を少なくすればいいのです。 – つまり、ページレンダリングの高速化を可能にするために最初の送受信で配信するコンテンツを最適化すればいいのです。   

    defer属性はどうでしょう?理論的には、deferはDOMContentLoadedイベントの前に特定の実行順序でスクリプトが実行されることを保証しなければなりません。残念ながら、deferのサポートと実装はバグが多く、信頼できません。例えば、IE8/IE9では実行できず、他のブラウザでは上手く動作しないこともあります。結果として、deferは実際のところ、実行可能な属性でも信頼できる属性でもありません。実行順序を維持する必要がある場合は、async属性付きにしてスクリプトローダーを使用してください。