scriptのdefer/asyncを理解し、ページの高速化方法を探る

Yslow ルールでは、スクリプトはページの最後尾、つまり </body> 直前に置け、と言っています。なぜなら、スクリプトの読み込みや実行により、他のページ要素の読み込みやレンダリングがブロックされてしまうからです。

一方、古くは IE4 の時代から Microsoft はこの問題に対処するため、defer 属性という独自の解決策を実装してきました。これは HTML 4.01、XHTML 1.0、1.1 で仕様として採用され、HTML5 にも発展する形で引き継がれています。

IE 以外のブラウザも既に対応されており、IE の独自仕様という色合いが濃かった従来と異なり、これからは広く利用されていくのではないかと思います。

下のビデオは、スクリプトの位置と defer 属性のあり/なしによる、ページの読み込み/表示速度の違いを Pagetest.com でテストしてみたもので、明らかな差異が分かると思います。

Loading the player …


本エントリーでは、scriptdefer/async 属性を正しく使えるよう、ブラウザの動作とブロッキングが起きる理由を理解し、HTML 仕様の確認と実験を交え、その効果的な使い方を探ってみたいと思います。

1.ブラウザの動作とブロッキング

Yahoo! のフロントエンド主任エンジニアである Nicholas C. ZakasWhat is a non-blocking script? の中で次のように述べています。

ブラウザは、JavaScript の実行かユーザーインターフェースのレンダリングか、どの時点においてもどちらか一方しか実行できません(両方を同時には出来ない)。これは論理的に理解できます。なぜなら、JavaScript は要素を動かしたり内容を変更することによってユーザーインターフェースに影響を与えることが出来るからです。この次にユーザーインターフェースを更新するときには、ブラウザは最新の情報が使われることを確実にしておきたいのです。

この前提知識を元に、ページがブラウザにダウンロードされる時に何が起きるか考えて見ましょう。ページがダウンロードされるとレンダリングが始まります。そして <script> タグに出会うと、JavaScript がユーザーインターフェースに影響を与える可能性があるため、ブラウザはそれ以上レンダリングをすることが出来なくなります。次に HTTP コネクションが張られ、JavaScript ファイルがダウンロードされ、評価・実行がされます。それらが一旦完了したときだけ、出力が最新であるという完全な確信の元に、ブラウザはページの残りをレンダリングすることが出来るのです。

同じく Yahoo! エンジニアの Stoyan StefanovJavaScript loading strategies の中で、次のように述べています。

ブラウザがサーバーから HTML ドキュメントを受信すると、最初に行うことは DOM を構築することです。この過程では、常に CSS 文書や画像などの外部の依存関係への参照が発生します。その依存関係解消のため、ブラウザはサーバーに要求を矢継ぎ早に発します。この時、1つの要求が完全にロードされるのを待つ必要は無く、可能な限りの多くの要求を出します。また1つのノード構築に際し、外部への依存関係があれば、それは適切なプレースホルダに入れられます。このような機構により順序性を保ちつつ DOM の構築を効率良く行うのです。

一方、この過程で JavaScript への依存がある場合、この機構は台無しになります。ブラウザは JavaScript に出会うと DOM の構築を停止し、そのファイルが到着するのを待ちます。そしてファイルを受信し、解析し、実行し、完了すると、再び DOM の構築を続けます。この機構は、スクリプトに対して、できるだけ安定した DOM を提供するためのものです。スクリプトが DOM ノードをアクセスしたり変更しようとするとき、それが流動的であるなら、いろいろな危険なことになるでしょう。

さて、defer/async の仕様にも関連する document.write() の例を示しておきます。

HTML
<script type="text/javascript" src="image1.js"></script>
<img src="http://example.com/static/image2.jpg" width=100 height=100 />
image1.js
var url = 'http://example.com/static/image1.jpg';
document.write('<img src="' + url + '"  width=100 height=100 />');

image1.js は、あたかもそこに元からあったかのように <img> タグを出力します。即ちページの上から順に DOM を構築していくためには、そこでスクリプトの読み込みと実行を待たなければならないのです。特に document.write() は実行の位置と順番が保証されるため、同期的 (synchronous) と言われます。

2.ドキュメントタイプとdefer/async属性の仕様

以下にドキュメントタイプ別の書式と DTD 仕様の出典を示します。

ドキュメントタイプ <script> の書式 DTD 仕様
HTML 4.01 <script defer ...>
HTML 1.0、1.1 <script defer="defer" ...>
HTML 5 <script defer ...>
<script async ...>

仕様上のポイントは次のようになります。

  • defer/async 付き script は、それ以降の DOM 構築とレンダリングをブロックしない
  • defer/async 付き script は、インラインスクリプトには使用できない (HTML 5)
  • defer/async 付き script の中で、document.write() は使用できない (HTML 4.01、XHTML 1.0、1.1)、または推奨しない (HTML 5)
  • defer 付き script は、「ドキュメントの Parse 終了後 に実行されるスクリプトのリスト」 の最後に追加される (HTML 5) → スクリプトファイルの読み込み後、実行はページの読み込み完了後まで遅延される
  • async 付き script は、「可能な限り早く実行されるスクリプトの集合」 に追加される (HTML 5) → スクリプトファイルの読み込み後、出来る限り早く実行される
Parse Model

Parse Model

上記の 「ドキュメントの Parse 終了後」 とは、HTML の解析と DOM 構築の完了を指します。詳しい解説が 10.2.1 Overview of the parsing model にありますが、ブラウザでいう onload イベントと同じタイミングなのかどうか、イマイチ読みきれていません 😉 。

さて、複数の <script>defer/async が付いた時の振る舞いはどうなるのでしょうか?仕様を読む限り次のようになります。

  • async は、読み込みが早く終了したスクリプトから非同期的に実行されますので、順序性は保証されない。即ち、依存するスクリプト同士に async を付けることが出来ない
  • defer は、「ページの読み込み終了後に実行されるスクリプトのリスト」 を元に順に評価・実行されるので、順序性 (= 同期性) が保たれるハズ。

defer の 「ハズ」 というのは、2010年10月7日に開催の Fronteers 2010 で発表された Nicholas C. Zakas のスライド High Performance JavaScript P.85 の情報によります。

タイミングに関する注意事項:
<script defer> は UI の更新が完了した後に必ず実行されるが、複数の defer 付き script は、全てのブラウザでその実行順序が保証されていません。

Nicholas C. Zakas は、Yahoo! のフロント・エンド・エンジニアの大家 (給料が 4000万!?) であり、無視できない情報と思っています。そのようなブラウザが特定出来次第、ここで報告したいと思います。

3.defer/asyncの対応状況

2010年12月4日 お断り

以前の記述には誤りが多かったため、削除しました。「defer/asyncチェックツールを作りブラウザ対応状況を確認してみた」 で再検証しましたので、そちらを参照願います。

4.defer/asyncの使い方

4.1インラインスクリプトによる処理のキックオフにイベントハンドラを使う

外部スクリプトの読み込みに続けて、その中で定義された関数をインラインスクリプトで呼び出す場合には、直接その関数を呼び出すとエラーとなりますので、window.onload に関数を登録します。

サンプルページ (1)

<script type="text/javascript" src="a.js" async></script>
<script type="text/javascript">
	window.onload = function() {
		// a.js に定義されたキックオフ関数
		kickoff();
	}
</script>

2013年4月29日 修正

今や window.onload は使いませんネ。(参考:EventTarget.addEventListener – DOM | MDN

サンプルページ (2)

<script type="text/javascript" src="a.js" async></script>
<script type="text/javascript">
	if (window.addEventListener) {
		window.addEventListener('load', function() {
			// a.js に定義されたキックオフ関数
			kickoff();
		}, false);
	} else {
		window.attachEvent('onload', function() {
			// a.js に定義されたキックオフ関数
			kickoff();
		});
	}
</script>

従来、window.onload より少しでも早いタイミングで JavaScript をキックオフできるように工夫されて来たので、ちょっとアレな感じです。次の様にすれば、スクリプトが読み込まれた直後にキックオフできるのですが、IE8 以下では動作しません。他のイベントを用いれば何とかなるかもしれませんネ。

サンプルページ (3)

<script type="text/javascript" src="a.js" async onload="kickoff()"></script>

2013年4月29日 修正

CSS/JavaScriptのAsynchronous Loadingをめぐる熱い論議」 の IE8 以下でも動作する onreadystatechange を使った例と、Google Analytics のトラッキングコード を参考にすると、次のようになります。

サンプルページ (4)

<script type="text/javascript">
function loadScript(src, callback) {
	var e = document.createElement('script');
	if (e.readyState) { // IE
		e.onreadystatechange = function() {
			if (e.readyState == 'loaded' || e.readyState == 'complete') {
				e.onreadystatechange = null;
				callback();
			}
		};
	} else { // Others
		e.onload = function() {
			callback();
		};
	}
	e.type = 'text/javascript';
	e.async = true;
	e.src = src;
	var s = document.getElementsByTagName('script')[0];
	s.parentNode.insertBefore(e, s);
}
// スクリプトの非同期読み込みと起動
loadScript('a.js', function() {
	kickoff();
});
</script>

4.2依存関係のない外部スクリプトには async が使える

例えば次の例では、Nivo Slider と Lightbox に依存関係がないため、それぞれ async を付けることが出来ます。 しかし両者は jQuery に依存するので jquery.js には何も付けることが出来ず、効果を発揮する場面は非常に限られます。

5月15日 修正

下のコードは嘘でした。お詫びして訂正します。たまたま jQuery の方が Google CDN からの読み込みで速かっただけで、依存関係の全くないスクリプト以外 async は使えません。

サンプルページ

<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery.nivo.slider.js" async></script>
<script type="text/javascript" src="jquery.lightbox-0.5.js" async></script>
<script type="text/javascript">
	$(document).ready(function() {
		$('#nivo-slider').nivoSlider();
		$('#nivo-slider a').lightBox();
	}
</script>

4.3ソーシャルブックマークなどの単独スクリプト

本サイトでは、ソーシャルブックマーク・サービスの AddThis スクリプトに defer="defer" を指定しています。こういったスクリプトは、ページの途中に置かざるを得ないので、有効だと思います。ただし、3rd パーティー製のスクリプトは document.write を使っていないかどうかを調べる必要があります。もし使われていたら、提供元に改善を求めるが吉です。AddThis の場合、古いスクリプトバージョン (120) で document.write が使われますが、新バージョン (250)なら大丈夫す。

4.4JavaScriptファイルの結合・最適化・圧縮を行うMinifyと同時に使う

WP MinifyでCSS/JavaScriptをまとめて軽量化、ページの応答速度を向上させる」 で紹介した Minify を使ってスクリプトを1つのファイルに結合すれば順序性を保つことが出来るので、defer/async と組み合わせれば効果絶大でしょう。 🙂 。

4.5<head>内のdefer/async付きスクリプトと、ページ最後尾のスクリプトは同じか?

微妙です。冒頭のビデオ (Pagetest.com では現在、IE7 を使っています) で示した4つのサンプルを Firefox 3.6 で観測したウォーターフロー図を示します。

<head> 内、かつ defer なし」 は別として、defer 付きの <script> の方が遥かに早く DOMContentLoaded イベントが発生し、DOM の構築が一気に進むことが分かります。ですが onload までの時間はほとんど変わりません。

スクリプトの位置 defer 属性 ウォーターフォール図 サンプル
<head> なし Head without defer Head without defer
<head> あり Head with defer Head with defer
</body> 直前 なし Bottom without defer Bottom without defer
</body> 直前 あり Bottom with defer Bottom with defer

<head> かつ defer 付き」 と 「ページ最後尾 かつ defer なし」 をさらにミクロに比較をすると、レンダリングの順番が違うことが分かります。JavaScript を補助的手段と考えると、メインのコンテンツが早く表示される前者のほうが望ましいと言うことも出来ます。

レンダリングの詳細

レンダリングの詳細

一方で、Steve Souders の2006年のスライド High performance web sites の P.64 には、「FF (FF2?) は defer 付きスクリプトでもレンダリングやダウンロードをブロックするし、IE (IE7?) でも僅かにブロックする、だからスクリプトは最後尾に置くんだ!」 とあります。

どちらを優先させるべきかはケース・バイ・ケースではないでしょうか?、とお茶を濁してこのエントリーを締めくくりたいと思います 😉 。

scriptのdefer/asyncを理解し、ページの高速化方法を探る」への1件のフィードバック

  1. ピンバック: サクッと使えた!サイト移行前やWordPressテーマ作成時に知っておきたい小技 | コムテブログ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません