公開日: 2014 年 3 月 31 日
クリティカル レンダリング パスのパフォーマンスに対するボトルネックを特定し、解消するには、陥りやすい落とし穴を理解しておく必要があります。一般的なパフォーマンス パターンを見分ける使い方ガイドは、ページの最適化に役立ちます。
クリティカル レンダリング パスを最適化する目的は、できる限り早くブラウザがページを描画できるようにすることです。ページの高速化は、エンゲージメントの向上、ページの閲覧回数の増加、コンバージョン率の改善につながります。訪問者が空白の画面を表示する時間を最小限に抑えるには、どのリソースをどの順序で読み込むかを最適化する必要があります。
このプロセスを説明するために、まずは最もシンプルなケースから始めて、徐々にリソースやスタイル、アプリケーション ロジックを追加してページを構築していきます。その過程で、ケースごとの最適化を行い、失敗しやすいポイントついても説明します。
ここまでは、リソース(CSS、JS、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 ミリ秒かかっています。青色の線の透過部分は、ブラウザが応答バイトを受け取っておらず、ネットワーク上で待機している時間を表します。一方、塗りつぶされた部分は、最初の応答バイトを受け取ってからダウンロードが完了するまでの時間を表します。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 の例とは異なり、CSSOM を構築するために CSS ファイルを取得して解析する必要もあります。レンダリング ツリーを構築するには、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 ラウンドトリップになります。