公開日: 2014 年 3 月 31 日
クリティカル レンダリング パスのパフォーマンスに対するボトルネックを特定し、解消するには、陥りやすい落とし穴を理解しておく必要があります。ページの最適化に役立つ一般的なパフォーマンス パターンを特定するためのガイド付きツアーです。
クリティカル レンダリング パスを最適化する目的は、できる限り早くブラウザがページを描画できるようにすることです。ページの高速化は、エンゲージメントの向上、ページの閲覧回数の増加、コンバージョン率の改善につながります。訪問者が何もない画面を見つめるだけの時間を最小限にするため、「どのリソースのどの順で読み込むか」を最適化することが必要です。
このプロセスを説明するために、まずは最もシンプルなケースから始めて、徐々にリソースやスタイル、アプリケーション ロジックを追加してページを構築していきます。その過程で、ケースごとの最適化を行い、失敗しやすいポイントついても説明します。
これまでは、リソース(CSS、JavaScript、HTML などのファイル)が処理できる状態になったあと、ブラウザ側で行われる処理だけに焦点を当てており、リソースをキャッシュから取得する場合とネットワークから取得する場合の所要時間については考慮していませんでした。ここでは、次の前提条件があるとします。
- サーバーまでのネットワーク ラウンドトリップ(プロパゲーション レイテンシ)は 100 ms
- サーバーの応答時間は、HTML ドキュメントの場合は 100 ms、その他のファイルの場合は 10 ms
Hello World サンプル
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
まずは CSS と JavaScript は使わずに、基本的な HTML マークアップと 1 つの画像から始めましょう。次に、Chrome DevTools でネットワーク ペインを開き、リソース ウォーターフォールを確認します。
想定どおり、HTML のダウンロードに約 200 ms かかっています。青色の線の透過部分は、ブラウザが応答バイトを受け取っておらず、ネットワーク上で待機している時間を表します。一方、塗りつぶされた部分は、最初の応答バイトを受け取ってからダウンロードが完了するまでの時間を表します。HTML のダウンロード量はわずか(4 K 未満)であるため、1 回のラウンドトリップでファイル全体を取得できます。そのため、HTML ドキュメントを取得するための所要時間は約 200 ms です。この時間の半分はネットワーク上で待機しており、残りの半分はサーバーの応答を待っています。
HTML コンテンツが利用可能になると、ブラウザはバイトを解析してトークンに変換し、DOM ツリーを構築します。DevTools の下の方には、便宜のために、DOMContentLoaded イベントの時間(216 ms)が表示されています。これは、青色の縦線に相当します。HTML ダウンロードの終了と青色の縦線(DOMContentLoaded)の差が、ブラウザで DOM ツリーを構築するのにかかった所要時間であり、今回の場合、数ミリ秒にすぎません。
「awesome photo」が domContentLoaded
イベントをブロックしていない点にも注目してください。ページの各アセットを待たずに、レンダリング ツリーの構築やページのレンダリングができることを示しています。最初のレンダリングを高速化する上で、すべてのリソースが必須というわけではありません。後で、クリティカル レンダリング パスに関するトピックで説明するように、一般に検討対象となるのは、HTML マークアップ、CSS、JavaScript です。画像は、初回のページ レンダリングをブロックしませんが、できる限り早く画像がレンダリングされるように配慮する必要はあります。
ただし、load
イベント(onload
とも呼ばれます)は画像によってブロックされます。DevTools では、onload
イベントは 335 ms 時点でレポートされています。onload
イベントは、ページに必要なすべてのリソースがダウンロードされ、処理が完了した時点を表します。この段階で、ブラウザの読み込み中マークの回転が止まります(ウォーターフォール上の赤色の縦線に相当)。
JavaScript と CSS をサンプルに追加する
「Hello World サンプル」ページは、一見するとシンプルに見えますが、内部ではさまざまな処理が実行されています。実際には HTML 以外の要素も必要になります。CSS スタイルシートと 1 つ以上のスクリプトを組み込んで、ページに相互作用を追加することはよくあります。この両者をサンプルに追加して、どうなるか見てみましょう。
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="timing.js"></script>
</body>
</html>
JavaScript と CSS を追加する前:
JavaScript と CSS あり:
外部 CSS ファイルと JavaScript ファイルを追加すると、2 つのリクエストがウォーターフォールに追加されます。ブラウザは、ほぼ同時にすべてをディスパッチしています。ただし、domContentLoaded
イベントと onload
イベントのタイミングの差は大幅に縮まっています。
なぜですか?
- 前述の HTML のみのサンプルとは異なり、今回は CSS ファイルを取得して解析し、CSSOM を構築する必要があります。また、レンダリング ツリーの構築には、DOM と CSSOM の両方が必要です。
- パーサーをブロックする JavaScript ファイルをページに追加したことで、CSS ファイルのダウンロードと解析が完了するまで、
domContentLoaded
イベントがブロックされています。この理由は、JavaScript が CSSOM に対してクエリを実行する場合があるため、JavaScript を実行する前に、CSS ファイルをブロックしてダウンロードを待つ必要があるためです。
外部スクリプトをインライン スクリプトに置き換えるとどうなるでしょうか。スクリプトをインラインでページに直接組み込んだとしても、CSSOM が構築されるまで、ブラウザはスクリプトを実行できません。つまり、インライン JavaScript もパーサー ブロックになります。
CSS をブロックしても、インライン スクリプトの方がページのレンダリングが高速になるでしょうか。試して、どうなるか確認してください。
外部 JavaScript:
インライン JavaScript:
リクエストは 1 つ減りますが、onload
と domContentLoaded
のタイミングは実質同じです。その理由は、ご存知のとおり、JavaScript がインラインであっても外部ファイルであっても、大きな違いはありません。どちらの場合も、ブラウザは script タグに遭遇するとブロックして、CSSOM が構築されるまで待機します。また、最初のサンプルでは、CSS と JavaScript がブラウザによって同時にダウンロードされ、ほぼ同時にダウンロードが完了していました。よって、今回の場合は JavaScript コードをインライン化しても、あまりメリットはありません。ただし、ページのレンダリングを高速化するための戦略はいくつかあります。
まず、前述したように、インライン スクリプトは常にパーサー ブロックですが、外部スクリプトは、async
属性を追加してパーサー ブロックではなくすことができます。インライン化を元に戻し、試してみましょう。
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Async</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script async src="timing.js"></script>
</body>
</html>
パーサー ブロック(外部)JavaScript:
非同期(外部)JavaScript:
よくなりました。domContentLoaded
イベントは HTML の解析後すぐに発行されています。ブラウザは JavaScript でブロックせず、他にパーサー ブロック スクリプトは存在しないため、CSSOM の構築も並列して処理できます。
別の方法として、CSS と JavaScript の両方をインライン化するというアプローチもあります。
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Inlined</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
</style>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
domContentLoaded
の時間は、前のサンプルとほぼ同じです。JavaScript を非同期にする代わりに、CSS と JavaScript の両方を直接ページにインライン化しています。これにより、HTML ページのサイズはかなり大きくなっていますが、すべてがページ内にあるため、ブラウザで外部リソースの取得を待つ必要がないというメリットがあります。
ご覧のとおり、非常に基本的なページであっても、クリティカル レンダリング パスを最適化するのは簡単ではありません。さまざまなリソース間の依存関係グラフを理解し、どのリソースが「クリティカル」かを特定し、それらのリソースをページに含める方法をさまざまな戦略から選択する必要があります。ただし、ページごとに違いがあるため、対策は 1 つではありません。最適な戦略を特定するには、このようなプロセスを自身で実践する必要があります。
では、これらのことを踏まえて、一般的なパフォーマンス パターンを特定してみましょう。
パフォーマンス パターン
最もシンプルなページは、CSS、JavaScript、その他のリソースを含まず、HTML マークアップだけで構成されているページです。このページをレンダリングするために、ブラウザはリクエストを開始し、HTML ドキュメントが届くのを待ち、それを解析し、DOM を構築して、ようやく画面上にレンダリングします。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
T0 と T1 の間の時間は、ネットワークとサーバーの処理時間を表します。ベストケースの場合(HTML ファイルが小さい場合)、1 回 のネットワーク ラウンドトリップでドキュメント全体を取得できます。ファイルが大きい場合は、TCP 転送プロトコルの仕組み上、必要なラウンドトリップ数が増える可能性があります。つまり、ベストケースにおいては、上記のページのクリティカル レンダリング パスは(最低で)1 回のラウンドトリップになります。
同じページで、外部 CSS ファイルを使用するケースを検討しましょう。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
繰り返しになりますが、HTML ドキュメントを取得する際はネットワーク ラウンドトリップが発生し、取得したマークアップから CSS ファイルも必要であることがわかります。つまり、ブラウザは画面上にページをレンダリングするために、サーバーに戻って CSS を取得する必要があります。結果的に、このページを表示するには最低 2 回のラウンドトリップが発生します。CSS ファイルの取得に複数回のラウンドトリップが必要になる場合があるので、「最低」で 2 回です。
クリティカル レンダリング パスを説明する用語を定義しておきましょう。
- クリティカル リソース: ページの最初のレンダリングをブロックする可能性のあるリソース。
- クリティカル パス長: すべてのクリティカル リソースを取得するために必要なラウンドトリップの回数または合計時間。
- クリティカル バイト数: ページの最初のレンダリングに必要なバイト数の合計。これは、すべてのクリティカル リソースの転送ファイルサイズの合計です。最初の例では、1 つの HTML ページに 1 つのクリティカル リソース(HTML ドキュメント)が含まれていました。クリティカル パス長も 1 つのネットワーク ラウンドトリップと同じでした(ファイルが小さいと仮定)。クリティカル バイト数の合計は、HTML ドキュメント自体の転送サイズにすぎませんでした。
では、これと前述の HTML と CSS のサンプルにおけるクリティカル パスの特徴を比較してみましょう。
- 2 個のクリティカル リソース
- 最小クリティカル パス長のラウンド トリップ数は 2 以上
- クリティカル バイト数は 9 KB
レンダリング ツリーの構築には、HTML と CSS の両方が必要です。そのため、HTML と CSS の両方がクリティカル リソースとなります。CSS は、ブラウザが HTML ドキュメントを取得した後にのみ取得可能になるため、クリティカル パス長は、最低で 2 ラウンドトリップとなります。クリティカル バイトは合計 9 KB です。
では、追加の JavaScript ファイルをサンプルに追加しましょう。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
app.js
を追加しました。これはページの外部 JavaScript アセットであり、パーサー ブロック リソース(クリティカル リソース)になります。さらに、JavaScript ファイルを実行するには、ブロックして CSSOM を待つ必要があります。JavaScript では CSSOM に対するクエリが可能であるため、ブラウザは、style.css
がダウンロードされて CSSOM が構築されるまで、一時停止します。
ところで、このページの「ネットワーク ウォーターフォール」を確認すると、CSS リクエストと JavaScript リクエストがほぼ同じタイミングで開始されていることがわかります。ブラウザは HTML を取得し、両方のリソースを発見して両方のリクエストを開始しています。そのため、上の図に示すページのクリティカル パスの特徴は、次のようになります。
- 3 個のクリティカル リソース
- 最小クリティカル パス長のラウンド トリップ数は 2 以上
- クリティカル バイト数は 11 KB
今回のクリティカル リソースは 3 つ、クリティカル バイトは合計で 11 KB です。ただし、CSS と JavaScript は同時に転送できるため、クリティカル パス長は変わらず 2 ラウンドトリップです。クリティカル レンダリング パスの特徴を把握すると、クリティカル リソースを特定し、ブラウザがリソースの取得をスケジューリングする方法を理解できるようになります。
サイト デベロッパーと会話したところ、ページに組み込まれている JavaScript はブロック不要であることがわかりました。スクリプト内のアナリティクスや他のコードは、ページのレンダリングをブロックする必要がないのです。よって、async
属性を <script>
要素に追加して、パーサーをブロックしないようにすることができます。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
非同期スクリプトにはいくつかメリットがあります。
- スクリプトがパーサー ブロックにならず、クリティカル レンダリング パスの一部ではなくなります。
- 他にクリティカル スクリプトがないため、CSS が
domContentLoaded
イベントをブロックする必要もなくなります。 domContentLoaded
イベントが早く発生するほど、他のアプリケーション ロジックも早く実行できるようになります。
この結果、最適化されたページでは、クリティカル リソースが 2 つ(HTML と CSS)に戻り、最小クリティカル パス長は 2 ラウンドトリップ、クリティカル バイト数は合計 9 KB です。
最後に、CSS スタイルシートが印刷にのみ必要なケースではどうなるのか見てみましょう。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" media="print" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
style.css リソースは印刷にのみ使用されるため、ブラウザでは、ページをレンダリングする際に CSS をブロックする必要がありません。したがって、DOM 構築が完了した時点で、ブラウザにはページのレンダリングに必要な情報がすべてそろっています。その結果、このページのクリティカル リソースは 1 つ(HTML ドキュメント)、最小クリティカル レンダリング パス長は 1 ラウンドトリップになります。