クリティカル レンダリング パスのパフォーマンスのボトルネックを特定して解決するには、よくある問題をよく理解しておく必要があります。ここでは、実践的なガイドに沿って、ページの最適化に役立つ一般的なパフォーマンス パターンの抽出にお役立てください。
クリティカル レンダリング パスを最適化すると、ブラウザはページを可能な限り早く描画できるようになります。ページの読み込みが速いほど、エンゲージメントが高まり、閲覧ページが増え、コンバージョン率が向上します。訪問者が空白の画面を表示する時間を最小限に抑えるには、どのリソースをどの順序で読み込むかを最適化する必要があります。
このプロセスをわかりやすく説明するために、まずは最もシンプルなケースから始めて、リソース、スタイル、アプリケーション ロジックを追加しながら段階的にページをビルドしていきます。このプロセスでは、各ケースを最適化し、どこで問題が発生するかも確認します。
ここまでは、リソース(CSS、JS、HTML ファイル)が処理可能になった後にブラウザで行われる処理のみに焦点を当ててきました。キャッシュまたはネットワークからリソースを取得するのにかかる時間は無視してあります。ここでは、次のように仮定します。
- サーバーへのネットワーク ラウンドトリップ(伝播レイテンシ)には 100 ミリ秒かかります。
- サーバーの応答時間は、HTML ドキュメントで 100 ミリ秒、他のすべてのファイルで 10 ミリ秒です。
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>
基本的な HTML マークアップと 1 つの画像から始めます。CSS や JavaScript は使用しません。Chrome DevTools でネットワーク タイムラインを開き、結果のリソース ウォーターフォールを調べてみましょう。
想定どおり、HTML ファイルのダウンロードに約 200 ミリ秒かかりました。青色の線の透明な部分は、ブラウザがレスポンス バイトを受信せずにネットワーク上で待機する時間の長さを表し、実線部分は、最初のレスポンス バイトを受信してからダウンロードを完了するまでの時間を示しています。HTML のダウンロードはサイズが小さい(4K 未満)ので、1 回のラウンドトリップでファイル全体を取得できます。その結果、HTML ドキュメントの取得に約 200 ミリ秒かかり、そのうち半分はネットワークで待機し、残りの半分はサーバー応答を待機します。
HTML コンテンツが使用可能になると、ブラウザはバイトを解析してトークンに変換し、DOM ツリーを構築します。DevTools の下部で、DOMContentLoaded イベントの時間(216 ミリ秒)が簡単に報告されます。これは青色の縦線に相当します。HTML ダウンロードの終了から青い縦線(DOMContentLoaded)までの間隔が、ブラウザが DOM ツリーを構築するのにかかる時間(この場合は数ミリ秒)です。
「Awesome Photos」によって domContentLoaded
イベントがブロックされていないことに注目してください。レンダリング ツリーを構築し、ページ上の各アセットを待たずにページをペイントすることもできます。最初のペイントを高速に行うためにすべてのリソースが重要というわけではありません。実際、クリティカル レンダリング パスといえば、通常は HTML マークアップ、CSS、JavaScript が話題になります。画像によってページの最初のレンダリングが妨げられることはありませんが、できるだけ早く画像をペイントするようにする必要もあります。
つまり、load
イベント(onload
とも呼ばれます)はイメージでブロックされます。DevTools は 335 ミリ秒で onload
イベントを報告します。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:
はるかに良くなりました。HTML が解析されるとすぐに domContentLoaded
イベントが発生します。ブラウザは 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 と JS の両方をページ自体にインライン化しています。これにより HTML ページがはるかに大きくなりますが、ブラウザが外部リソースを取得するのを待つ必要がなくなり、すべてがページ内に表示されるという利点があります。
ご覧のとおり、非常にシンプルなページであっても、クリティカル レンダリング パスの最適化は簡単な作業ではありません。異なるリソース間の依存関係グラフを理解し、どのリソースが「クリティカル」かを特定し、それらのリソースをページに含める方法についてさまざまな戦略の中から選択する必要があります。この問題の解決策は 1 つではなく、ページもそれぞれ異なります。最適な戦略を見つけるには、同様のプロセスを自分自身で行う必要があります。
とは言え、一歩引いて一般的なパフォーマンス パターンを特定できるか見てみましょう。
パフォーマンス パターン
最もシンプルなページは、HTML マークアップのみで構成され、CSS や JavaScript などのリソースは含んでいません。このページをレンダリングするには、ブラウザでリクエストを開始し、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 ラウンド(最低)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 ファイルでは複数回のラウンドトリップが必要になる場合があるため、「最小」を重視します。
クリティカル レンダリング パスの説明に使用する語彙を定義しましょう。
- 重要なリソース: ページの最初のレンダリングをブロックする可能性のあるリソース。
- クリティカル パスの長さ: すべてのクリティカル リソースを取得するために必要なラウンドトリップ回数または合計時間。
- クリティカル バイト: ページを最初にレンダリングするために必要な合計バイト数(すべてのクリティカル リソースの転送ファイルサイズの合計)。 HTML ページが 1 つで、クリティカル リソース(HTML ドキュメント)が 1 つある 1 つ目の例では、クリティカル パスの長さも 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」属性を追加すると、パーサーのブロックを解除できます。
<!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 リソースは印刷にのみ使用されるので、ブラウザはページをレンダリングするためにブロックする必要はありません。したがって、DOM の構築が完了すると、ブラウザはページをレンダリングするのに十分な情報を得ることができます。そのため、このページにはクリティカル リソース(HTML ドキュメント)が 1 つしかなく、クリティカル レンダリング パスの最小長は 1 往復です。