クリティカル レンダリング パスのパフォーマンスを分析する

クリティカル レンダリング パスのパフォーマンスに対するボトルネックを特定し、解決するには、さまざまなよくある落とし穴に関する正しい理解が必要となります。実践的なツアーを通じ、ページの最適化につながるパフォーマンス パターンの定型を見いだしましょう。

クリティカル レンダリング パス最適化の目標は、できる限り早くブラウザがページのレンダリングを完了できるようにすることです。ページの高速化は、エンゲージメントの増加、表示ページ数の増加、コンバージョンの向上につながります。訪問者が何もない画面をただ見つめるだけの時間を最小限にするため、「どのリソースのどの順で読み込むか」を最適化することが必要です。

このプロセスを理解するため、シンプルなケースから始め、徐々にリソースやスタイル、アプリケーション ロジックを追加して、ページを構築していきます。このプロセスにおいて、問題が生じるポイントや、ケースごとの最適化方法について身に付けましょう。

開始する前に注意点があります。これまでは、リソース(CSS、JavaScript、HTML などのファイル)が処理できるようになったときにブラウザ内で生じることだけに焦点を当てており、各リソースをキャッシュから取得する場合とネットワークから取得する場合の取得時間の差については無視してきました。ネットワークに関係するアプリケーション要素の最適化については、次のトピックで詳細に扱います。ここでは、現実世界に近づける中間ステップとして、次の点を前提とします。

  • サーバーに対するネットワーク ラウンドトリップ(プロパゲーション ディレイ)が 100 ms かかる
  • サーバー応答時間が、HTML ドキュメントの場合 100 ms、他のファイルの場合 10 ms かかる

Hello World サンプル

<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 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 つまたは複数のスクリプトを組み込むことで、ページのインタラクティブ機能を追加できます。両方とも追加し、何が起きるか確認しましょう。

<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を追加する前にを追加する前に:

DOM CRP

JavaScript と CSS あり:

DOM、CSSOM、JavaScript

外部の CSS ファイルと JavaScript ファイルを追加すると、ウォーターフォールには 2 つのリクエストが追加されます。どちらもほぼ同じタイミングでブラウザによってディスパッチされます。ここまでは問題ありません。ところが、domContentLoaded イベントと onload イベントのタイミングの差が大幅に縮まっています。何が起きたのでしょう?

  • シンプルな HTML サンプルとは異なり、CSS ファイルを取得して解析し、CSSOM を構築する必要があります。また、レンダリング ツリーを構築するため、DOM と CSSOM が両方とも必要です。
  • パーサーをブロックする JavaScript ファイルをページに追加したため、CSS ファイルのダウンロードと解析が完了するまで、domContentLoaded イベントが、ブロックされています。JavaScript は CSSOM に対してクエリを行うことがあるため、JavaScript を実行するには、ブロックして CSS を待つ必要があります。

外部スクリプトをインライン スクリプトに置き換えた場合は、どうなるでしょうか。表面的にはつまらない質問ですが、実は非常に複雑です。スクリプトをインラインとしてページに直接組み込んだ場合、そのスクリプトが何を実行しようとしているのかブラウザが認識するには、実際に実行する以外に信頼できる方法がありません。しかし、説明したとおり、CSSOM が構築されなければ、この方法を行うことはできません。つまり、インライン JavaScript もパーサー ブロックとなります。

ただし、CSS をブロックする点は同じですが、インライン スクリプトの方がページのレンダリングが高速になります。この点は、先ほどの説明よりもさらに複雑な説明が必要となります。実際に試して、何が起こるのか確認してみましょう。

外部 JavaScript:

DOM、CSSOM、JavaScript

インライン JavaScript:

DOM、CSSOM、インライン JavaScript

リクエストが 1 つ少なくなりますが、onload と domContentLoaded の時間はほとんど同じです。なぜでしょう。説明したとおり、JavaScript がインラインであっても外部ファイルであっても、大きな違いはありません。どちらの場合も、ブラウザが script タグに遭遇すると、ブロックして、CSSOM が構築されるまで待ちます。最初のサンプルでは、CSS と JavaScript が並列してブラウザによってダウンロードされており、ほぼ同じ時間に完了しています。そのため、今回のサンプルでは、JavaScript コードをインライン化しても、それほど役に立っていません。では、ページのレンダリングを高速にするには、どのようにすればよいのでしょうか。いくつかの戦略があります。

まず、インライン スクリプトは常にパーサー ブロックですが、外部スクリプトは、async キーワードを追加することで、パーサーをブロックしないように設定できます。インライン化を元に戻し、試してみましょう。

<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:

DOM、CSSOM、JavaScript

非同期(外部)JavaScript:

DOM、CSSOM、非同期 JavaScript

パフォーマンスが大幅に改善されました。domContentLoaded イベントは HTML の解析後すぐに発行されており、ブラウザは JavaScript をブロックせず、他にパーサー ブロック スクリプトは存在しないため、CSSOM 構築も並列して処理されています。

別の方法として、CSS と JavaScript を両方ともインライン化するというアプローチがあります。

<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>

DOM、インライン CSS、インライン JavaScript

domContentLoaded 時間は、前のサンプルとほとんど同じですが、JavaScript を非同期化する代わりに、CSS と JavaScript を両方ともページ自体にインライン化しています。そのため、HTML ページははるかに大きくなっていますが、ブラウザが外部リソースを取得するのを待たなくてもよいというメリットがあります。すべてがページの中にあります。

以上のように、非常にシンプルなページでも、クリティカル レンダリング パスの最適化は非常に複雑です。さまざまなリソース間の依存関係図を把握し、どのリソースが「クリティカル」であるか特定し、そのようなリソースをページに組み込む方法をさまざまな戦略の中から選び出す必要があります。この問題に対して単一の解決策はありません。ページごとに違いがあり、最適な戦略を把握するには、自分自身で同様のプロセスを実践する必要があります。

では、少し戻って、パフォーマンス パターンについて考えてみましょう。

パフォーマンス パターン

最もシンプルなページとして、HTML マークアップだけのページを想定します。CSS や JavaScript といった他のリソースは存在しません。このページをレンダリングするには、ブラウザは、リクエストを開始し、HTML ドキュメントが到着するのを待ち、それを解析し、DOM を構築し、最後に画面上にレンダリングします。

<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>

Hello World CRP

T0 と T1 の間の時間は、ネットワークとサーバーの処理時間を示します。最高の条件の場合(HTML ファイルが小さい場合)、ドキュメント全体を取得するのに必要なネットワーク ラウンドトリップは 1 回だけです。TCP 転送プロトコルの仕組みにより、大きなファイルの場合は、必要なラウンドトリップ数が増えることがあります。この点については、後のトピックで扱います。このトピックで扱う上記のページの場合、最高の条件が当てはまり、ラウンドトリップ 1 回(最少)のクリティカル レンダリング パスになります。

同じページで、外部 CSS ファイルを使用するケースを想定します。

<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>

DOM + CSSOM CRP

HTML ドキュメントを取得するためのネットワーク ラウンドトリップが発生し、取得したマークアップに「CSS が必要」と記されている場合、ブラウザはサーバーに戻り、CSS を取得してから、画面上にページをレンダリングします。そのため、このページを表示するには、最低 2 回のラウンドトリップが発生します。CSS ファイルの取得に複数回のラウンドトリップが必要となる場合があるので、「最低」で 2 回です。

クリティカル レンダリング パスを説明する用語の定義は次のとおりです。

  • クリティカル リソース: ページの最初のレンダリングをブロックする可能性のあるリソース。
  • クリティカル パス長: すべてのクリティカル リソースを取得するのに必要とされるラウンドトリップ数または総時間。
  • クリティカル バイト: ページの最初のレンダリングに必要な総バイト数。すべてのクリティカル リソースの転送ファイルサイズを合計した値となります。 最初のサンプルの場合、1 つのクリティカル リソース(HTML ドキュメント)を含む単一の HTML ページであり、クリティカル パス長は、1 ネットワーク ラウンドトリップ(ファイルは小さいと想定)となり、総クリティカル バイトは、HTML ドキュメント自体の転送サイズだけとなります。

では、上記に示した HTML + CSS のサンプルの場合、クリティカル パスの特徴はどのようになるか、比較してみましょう。

DOM + CSSOM CRP

  • クリティカル リソースは 2
  • 最低のクリティカル パス長として 2 回以上のラウンドトリップ
  • クリティカル バイトは 9 KB

レンダリング ツリーの構築には、HTML と CSS が両方とも必要です。そのため、HTML と CSS の両方がクリティカル リソースとなります。CSS は、ブラウザが HTML ドキュメントを取得した後にのみ取得可能になるため、クリティカル パス長は、最低で 2 ラウンドトリップとなります。クリティカル バイトは総計で 9 KB となります。

では、外部 JavaScript ファイルを組み込んでみましょう。

<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>

ページの外部 JavaScript アセットとして app.js を追加しました。これはパーサー ブロック リソースであり、クリティカル リソースとなります。さらに、JavaScript ファイルを実行するには、ブロックして、CSSOM を待つ必要があります。説明したとおり、JavaScript は CSSOM に対してクエリを行う可能性があるため、ブラウザは、「style.css」のダウンロードが完了して CSSOM が構築されるまで、一時中断します。

DOM、CSSOM、JavaScript CRP

このページの「ネットワーク ウォーターフォール」を確認すると、CSS リクエストと JavaScript リクエストがほぼ同じタイミングで開始されていることがわかります。ブラウザは、HTML を取得し、両方のリソースを発見すると、両方のリクエストを開始します。そのため、上記のページのクリティカル パスの特徴は、次のようになります。

  • クリティカル リソースは 3
  • 最低のクリティカル パス長として 2 回以上のラウンドトリップ
  • クリティカル バイトは 11 KB

クリティカル リソースは 3 つになり、クリティカル バイトは総計で 11 KB になります。ただし、クリティカル パス長は、2 ラウンドトリップのままです。CSS と JavaScript が並列して転送できるためです。クリティカル レンダリング パスの特徴を理解すると、クリティカル リソースはどれか特定できるようになり、ブラウザがリソース取得のスケジュールをどのように設定するのか理解できるようになります。サンプルの考察を続けましょう。

サイト デベロッパーと相談したところ、ページに組み込んだ JavaScript によるブロックが不要であることが判明しました。アナリティクスや他のコードがあり、この場合は、ページのレンダリングをブロックする必要はありません。そこで、script タグに async 属性を追加し、パーサーをブロックしないようにします。

<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>

DOM、CSSOM、非同期 JavaScript CRP

スクリプトの非同期化には、いくつかのメリットがあります。

  • スクリプトがパーサー ブロックにならず、クリティカル レンダリング パスの一部ではなくなります。
  • 他にクリティカル スクリプトがないため、CSS が domContentLoaded イベントをブロックする必要もなくなります。
  • domContentLoaded イベントの発行が早くなれば、他のアプリケーション ロジックの実行も早く行われるようになります。

したがって、最適化されたページは、2 つのクリティカル リソース(HTML と CSS)に戻り、クリティカル パス長は最低で 2 ラウンドトリップとなり、クリティカル バイトは総計で 9 KB になります。

最後に、CSS スタイルシートが印刷時にのみ必要なケースを考えてみましょう。どのようになるでしょうか。

<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>

DOM、非ブロック CSS、非同期 JavaScript CRP

style.css リソースは印刷時にのみ使用されるため、ブラウザは、ページをレンダリングする際にブロックする必要がなくなります。そのため、DOM 構築が完了すると、ブラウザは、ページのレンダリングに必要な情報をすべて手に入れることになります。その結果、このページの場合、クリティカル リソースは 1 つ(HTML ドキュメント)、クリティカル レンダリング パス長は最低で 1 ラウンドトリップとなります。