ContentLoded.com

サイトに適したリソース配置とasync/defer完全マスター – レンダリング優先のグッド・プラクティス

タイトル、少し変えました 😉 。

第1部「サイト高速化の「戦略」と「戦術」- GradeAのその先へ」では、YSlow や PageSpeed がアドバイスする Tips のうち、HTTP リクエストの削減を優先すべしという「戦略」の話をしました。

また css や js を束ねて結合し、HTTP リクエストを削減する時の「戦術」の話もしました。

今回は、「サイトの特性に適したリソースの配置を行う」ために、「束ねたリソースをドキュメント中にどう配置するのが適切か」を見い出したいと思います。またそのポイントとなるブラウザの基本的な挙動についても言及します。

ブラウザごとの挙動が確かめられる、実験サイト contentloaded.com を立ち上げたので、以下、同サイトから幾つかの例を引きながら話を進めたいと思います。

リソース配置を決める戦術

基本的な戦術

第1部 では、css や js を束ねる時の戦術として、次の3つを挙げました。

  1. Above the fold に関連するリソースを識別する
  2. サイト共通のリソースとページ個別のリソースを識別する
  3. 各リソース間の依存関係や疎密関係を識別する

これらに対応し、特にスクリプトについて次の3つの戦術でその配置を決め、「ベスト・プラクティスとレンダリング優先を両立させるリソース配置」を狙います。

  1. Above the fold に直結するスクリプトは、最少限を <head> に配置する
  2. 残りのスクリプトはベスト・プラクティスに従い </body> 直前に配置する
  3. 各スクリプトには、可能な限り積極的に asyncdefer を付ける

戦術 1. は、第1部で触れたように「真に動的な部分はどこか」を見極めた上で、「Above the fold のコンテンツは、可能な限りサーバーサイドで構築」し、動的な部分だけをスクリプトでレンダリングします。

また戦術 3. は、リソース数が多いままだとエラーの元になりますし、リソース間の依存関係がハッキリしていないと付けられません。この意味でも、束ねる時の戦術「各リソース間の依存関係や疎密関係を識別する」がポイントとなります。

また「スクリプトの依存関係や実行順序、起動タイミングを適切に設計する」ことも重要です。このために、async 属性や defer 属性を付けたときのブラウザの挙動を把握します。

ブロッキングの挙動と非同期化

先の戦術を成功に導くキーポイントは、ブラウザの「ブロッキングの挙動」とそれを回避する「非同期化」です。Google Analytics の トラッキング・コード を契機に、非同期化が他のサービスにも広がったのはご存知でしょう。

ところが意外に、自身がホストするリソースの非同期化はあまり進んでいないように思います。asyncdefer といった属性が古くからあるにも関わらず、です。なぜでしょう?

理由の1つに、複雑な ブラウザの仕組み、特にブロッキングの挙動への理解不足が挙げられると思います。同時に、ページ、特にスタイルシートやスクリプトの構成が複雑なため、asyncdefer の活用戦術が立てにくいことも、理由の1つでしょう。

逆に、ブロッキングの基本的な挙動を理解し、css や js の構成をシンプルにすれば、asyncdefer の効果的な活用方法が見えてきます。

そこで次章では、css や js のブロッキングについて掘り下げ、その挙動の理解を試みたいと思います。

参考情報

  1. 2011年08月05日 ブラウザのしくみ: 最新ウェブブラウザの内部構造 – HTML5 Rocks

ブラウザの基本的な挙動

css は js より先の方が高速?

唐突ですが、クイズです 😀 。

<head> 内の css と js の配置に関して、正しいのはどちらでしょう?

  • css を先に、js を後に配置した方が高速
  • 最新のブラウザではむしろ js を先に配置した方が高速

ベスト・プラクティスでは、「<head> 内では css 用 <link> タグは <script> タグより先に書け」と言われます。

一方 2012年02月14日 の Stack Overflow には、「<head> 内では <script> を先にした方が高速」という実験結果を元に、「css を js より先に読み込めというベスト・プラクティスは間違いでは?」という疑問が投げかけられています。

これに対し、2つの興味深い回答が提示されています。

Steve Souders の回答
  1. 古いブラウザ(IE6, 7 や Firefox 2 など)は、js は ページの解析をブロックするので、js を先にすると css の読み込みが直列になる。css を先に書けば、並列読み込みとなる。
  2. どんなブラウザでも css の読み込みが終わらない限り、レンダリングがブロックされる。
josh3736 の回答
  1. デスクトップのブラウザ(IE8 を含む)は speculative parsing (無駄になる可能性を承知でページの 投機的な解析 を進める仕組みのこと。主スレッドとは別に並列で進められ、うまくいけばマージされる)により、css を先にするメリットはなく、むしろ js を先にした方が失うこと(レンダリングが遅れること)が少ない。
  2. ただしモバイルでは Android 4 でさえ、speculative parsing は実装されていないようだ。

モバイルの状況を考慮すると、しばらくはベスト・プラクティスに従っておくのが良さそうです。しかし現在の「常識」は、いずれ修正されるかもしれませんネ。

css によるブロッキング

ページの解析やレンダリングのブロックは js だけの挙動ではありません。css にもあります。次は、<head> 内での読み込みに、css が1秒、js が0.5秒かかる例 です。

<head>
    <link href="style.css" rel="stylesheet">
    <script src="script.js"></script>
</head>

head style script

上図は Firefox (27) のものですが、css の読み込みと解析が完了するまで js の実行がブロックされていることが分かります。

css や js によるブロッキングは、「スクリプトが document.write() でタグを吐くケース」を想定すると、ブラウザがそれを必要とする理由に合点がいくことが多々あります。

下の図は、Chrome Dev Summit 2013 で発表された Bryan McQuade さんのプレゼン Instant Mobile Web Apps から引用させてもらった図です。

What do you mean 'CSS is blocking'?

この図が示唆するのは、js の読み込みと実行がレンダリングをブロックするのと同様、css もその解析が終了するまでレンダリングがブロックするということです。

加えて、document.write()<style> タグを出力する可能性があるため、CSSOM 構築中は、js は並列に読み込めても実行まではできないことになります。また js が非常に短いインラインで展開できるなら、css より js を先に実行した方が、レンダリングのブロック時間も短くなる場合だってあります。

先の「css より js を先にした方が高速」という件は、この css と js 相互のブロッキングが絡んでいるのではないかと私は睨んでいます。

js によるブロッキングとその回避

次に <body> に画像を置いた例 を見てみます。

<head>
    <link href="style.css" rel="stylesheet">
    <script src="script.js"></script>
</head>
<body>
    <img src="html.png">
    <img src="watch.png">
</body>

head style script image

css が js の実行をブロックし、さらに js が画像要素の DOM 構築をブロックしている様子が見て取れます。

※ Chrome や Opera の場合は、先の「投機的な解析」が激しく、より並列化されています。Chrome のデスクトップ版iOS 版Opera のデスクトップ版 と比較してみて下さい。

この場合 js に defer を付ける と、スクリプトによる DOM 構築のブロッキングが回避され、画像要素 html.pngwatch.png の DOM 構築が早まります。

<head>
    <link href="style.css" rel="stylesheet">
    <script src="script.js" defer></script>
</head>
<body>
    <img src="html.png">
    <img src="watch.png">
</body>

head style defer image

さらに async を付ける と、css によるブロッキングも回避され、スクリプトの実行タイミングを早めることができます。

head style async image

asyncdeferは、document.write() が使われていないことを宣言する属性です(詳しい説明は、async/defer属性とDOM構築 をご覧ください)。その結果ブラウザは、スクリプトの読み込み中も安心してページの解析と DOM 構築を進めることができるというワケです。

PageSpeed InsightsRemove Render-Blocking JavaScripts にも、<head> に置くスクリプトに async 属性を付ける例が紹介されています。

Load script asynchronously

ズバリ、「ブロッキング回避のカギは、async 属性や defer 属性の使い方にある」です。

FOUC

次は、ベスト・プラクティスに従いスクリプトを </body> 直前にまとめて配置した時 に起こりがちな FOUC の例です。

<head>
    <link href="style.css" rel="stylesheet">
</head>
<body>
    <img src="html.png">
    <img src="watch.png">
    <script src="jquery.js"></script>
    <script src="script.js"></script>
</body>

body image jquery script

この配置では、</body> 直前のスクリプトまで DOM 構築が順調に進みレンダリングも早期に実施されますが、スクリプトがスタイルを変更すると FOUC が発生します。

これを防止するには、最小限のスクリプトを asyncdefer 付きで <head> に置き、残りを </body> 直前に配置する ことです。

<head>
    <link href="style.css" rel="stylesheet">
    <script src="script.js" async defer></script>
</head>
<body>
    <img src="html.png">
    <img src="watch.png">
    <script src="jquery.js" async defer></script>
</body>

head async body image async

つまり第1の戦術「Above the fold に直結するスクリプトは、最少限を <head> に配置する」と第2の戦術「残りのスクリプトはベスト・プラクティスに従い </body> 直前に配置する」を組み合わせることで、<head> 内での css と js 相互のブロッキングを回避し、FOUC の影響を最小限にすることができるのです。

ベスト・プラクティスと(スクリプトによる)レンダリング優先を両立させるリソース配置」、これがこの章のポイントです。

参考情報

  1. 2012年09月28日 Google Analytics トラッキングスニペット再考(2012年モバイル版) — MOL
    • GA の非同期コードを <head> に置くべきか、</body> 直前に置くべきを検討している記事です。

スクリプトの依存関係と起動タイミング

オッとイケナイ、既に「実装」の話に入ってしまいましたネ。

依存関係とasyncdefer

依存関係のあるスクリプト同士には defer を、なければ async を付けることができます。

ここでは、jquery.js とそれに依存する script.js を想定した、次の2つのパターンを比べてみることにしましょう。

script.js にだけ asyncdefer を付ける
    ...
    <script src="jquery.js"></script>
    <script src="script.js" async defer></script>
</body>
jquery.jsscript.js の両方に defer を付ける
    ...
    <script src="jquery.js" defer></script>
    <script src="script.js" defer></script>
</body>

どちらも正しく動作しますが、何の属性も付いてないスクリプトがある前者より、何かしらの「非同期化」属性が付いている後者の方が、DOMContentLoaded の発火が早くなります。

まぁ、jQuery は CDN から引っ張ることが多いハズですので、実際には両者の差はわずかですし、DOM Ready のタイミングを早くしても意味がない場合もあります。

しかし、第3の戦術「各スクリプトには、可能な限り積極的に asyncdefer を付ける」ためにも、第1部で述べた戦術「各リソース間の依存関係や疎密関係を識別する」ことが重要です。

起動タイミング

従来 <head> に置くスクリプトは、jQuery の $(document).ready() などを使って起動するのが常套手段でした。なぜならその読み込みと実行が完了するまで <body> 中の DOM 構築がブロックされるからです。

しかし asyncdefer を付けてブロッキングを回避したり、</body> 直前に置くなら、DOM Ready を待たなくても、より早いタイミングで実行可能な場合があります。

そんな時は(ちょっと冗長ですが)、例えば次のようにタイミングに応じて起動できるようにしておくと、少なくともテスト段階では色々と試せて便利かと思います。

var id = "操作対象の要素ID";
if (document.getElementById(id)) {
    // ダイレクトに実行
    doMyJob(id);
} else if (typeof $ !== 'undefined') {
    // DOM Ready を待って実行
    $(function() {
        doMyJob(id);
    });
} else {
    // onload を待って実行
    addLoadEvent(window, doMyJob, id);
}

function addLoadEvent(window, callback) {
    var args = ([].slice.call(arguments)).slice(1);
    if (window.addEventListener) {
        window.addEventListener('load', function () {
            callback.apply({}, args);
        }, false);
    } else {
        window.attachEvent('onload', function () {
            callback.apply({}, args);
        });
    }
}

一方、1つのタイミングですべてのスクリプトを起動させると「重く」感じることもあり得るでしょう。ブラウザは、画像などの外部リソースが到着し次第、せっせとレンダリングに励みます。その忙しい最中に、さらに負荷をかけるのは得策でないこともしばしばです。

そんな時はあえて onload 後に廻すという手もアリです。例えばソーシャル・ボタン系スクリプトの読み込みや、Disqus コメンティング・システムの読み込みなどがその候補でしょう。

微妙な話:束ねるべき? 分けるべき?

css と js は、1ページ当たり各々1本にまで減らせれば究極でしょう。しかし1本に束ねてしまうと「横」を短くすることにはなりません。1ドメインあたりの同時接続数(HTTP/1.1HTTP/1.0)は有効に使うべきです。

直列? 並列?

しかしやはりリソース数が多いとネットワークに影響される確率も増えますし、場合によっては並列にならないこともあるので、やはり少ないに越したことはないと思います。

まとめと仕上げ

「戦略」だの「戦術」だの、少々大げさではありますが、要は、HTTP リクエストを削減するためにサイトの特性に合わせて css や js ファイルを束ね、ブラウザの基本的な挙動を知った上でそれら配置を決めるとイイという話をしてきました。

さらに css や js のブロッキングを回避する asyncdefer の使い方がカギで、実行順序や起動タイミングを適切に設計する上で知っておくべきブラウザの挙動について、下記がポイントであることを示しました。

  • ブラウザは、常に document.write() の可能性に備えなければならない
  • asyncdefer により、安心してページの解析と DOM 構築を進められる

一方「縦を短くする」には CSS スプライトに画像をまとめる などへの手当ても必須です。その上で YSlow の Make fewer HTTP requests が A であればゴールは目前です。

あとは「横を短くする」課題を全力でやっつけましょう 😀 。

「横」の施策で外せない、かつ効果が高いのが、キャッシュ有効期限の設定と gzip 圧縮です。ググれば山ほど出てきますが、私的なオススメは HTML5 Boilerplate に添付の Appache 用 .htaccess の設定です。サーバーに合わせて「Compression」と「Expires headers」を設定すればほぼ完成です。

さらに CDN が導入できれば鬼に金棒でしょう!
(なんという手抜き… スミマセン m_O_m

さてさて、現実はそう理屈通りにうまくいかないものです。そこで次回第3部では、WordPress を例題として補助ツールやプラグインを導入し、なるべく効率よく理屈通りに実践する方法を紹介したいと思います。

お楽しみに 😀 。

コメントを残す

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

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