JavaScript のインタラクティブ機能を追加する

JavaScript を利用すると、コンテンツやスタイル、ユーザー操作に対する挙動など、ページの大部分の要素を変更できるようになります。ただし、JavaScript は DOM 構築をブロックし、ページのレンダリング時に遅延を引き起こすことがあります。JavaScript を非同期化して、クリティカル レンダリング パスから不要な JavaScript を取り除き、最適なパフォーマンスを実現するようにします。

TL;DR

  • JavaScript は、DOM と CSSOM のクエリと変更を行うことができます。
  • JavaScript の実行は CSSOM をブロックします。
  • JavaScript は、明示的に非同期化を宣言していない場合、DOM 構築をブロックします。

JavaScript は、ブラウザ内で実行される動的言語で、ページの挙動に関する大部分の要素を変更可能にします。DOM ツリーに対して要素の追加や削除を行うことでページのコンテンツを変更したり、各要素の CSSOM プロパティを変更したり、ユーザー入力を処理したり、さまざまなことが可能になります。実践的に理解するため、以前使用した「Hello World」サンプルにシンプルなインライン スクリプトを追加してみましょう。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script</title>
  </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>
  • JavaScript により、DOM に到達し、隠れた span ノードへの参照を引き出すことができます。このノードはレンダリング ツリーでは見えませんが、確かに DOM の中に存在しています。参照を取得したら、そのテキストを変更できます(.textContent 経由)。さらに、計算処理済みの表示スタイル プロパティを「none」から「inline」にオーバーライドすることもできます。すべての記述と作業が完了すると、サンプルページには「Hello interactive students!」と表示されます。

  • JavaScript を利用すると、DOM に対して要素の新規作成、スタイル設定、追加、削除を行うことができます。技術的には、ページ全体を単一の大きな JavaScript ファイルにして、要素の作成とスタイル設定を 1 つずつ行うこともできます。ただし、HTML や CSS と連携させる方がはるかに容易に実践できます。サンプル JavaScript 関数の後半では、新しい div 要素を作成し、テキスト コンテンツの設定、スタイルの設定を行って、body に追加しています。

ページ プレビュー

これにより、既存の DOM ノードのコンテンツや CSS スタイルが変更され、まったく新しいノードがドキュメントに追加されました。このサンプルページがデザイン賞を受賞することはないでしょうが、JavaScript の持つ力と柔軟性は明確に示されています。

ただし、パフォーマンス面では大きな問題が隠されています。JavaScript は非常に強力ですが、ページ レンダリングの方法やタイミングについて大きな制限が加わります。

まず、上記のサンプルで、インライン スクリプトがページの下部にある点に注目してください。なぜでしょうか。実際に試せばすぐにわかると思いますが、このスクリプトを span 要素の上部に移動すると、スクリプトが成功せず、getElementsByTagName(‘span')null を返します。つまり、ドキュメント内に span 要素への参照が見つからないというエラーです。これは、重要なポイントを示しています。スクリプトは、ドキュメント内に挿入されたまさにその位置で実行されているということです。HTML パーサーが script タグに遭遇すると、DOM 構築のプロセスを一時中断し、JavaScript エンジンに制御権を渡します。JavaScript エンジンの実行が完了すると、ブラウザは中断前の位置から DOM 構築を再開します。

つまり、スクリプト ブロックは、script タグ以降のページの要素を見つけることはできません。まだ処理されていないためです。結局、インライン スクリプトの実行は、DOM 構築をブロックし、最初のレンダリングも遅らせることになります。

また、サンプルページにスクリプトを導入したことで、DOM だけでなく CSSOM プロパティも、スクリプトによる読み込みと変更が可能であることが判明しました。サンプルでは、span 要素の display プロパティを none から inline に変更しています。その結果、何が生じたでしょうか。競合状態が生まれました。

スクリプトを実行させようとするときに、ブラウザが CSSOM ダウンロードと構築を完了していないと、どうなるでしょうか。答えは簡単で、パフォーマンスに悪影響が生じます。ブラウザは、CSSOM のダウンロードと構築が完了するまで、スクリプトの実行を遅らせ、待っている間は DOM 構築もブロックされます。

つまり、JavaScript によって、DOM、CSSOM、JavaScript の実行の間で新たな依存関係が大量に生まれます。できるだけ早く画面にページを表示させたくても、ブラウザの処理とレンダリングの時間が大幅に遅れる場合があります。

  1. ドキュメント内のスクリプトの位置が重要です。
  2. DOM 構築は、script タグに遭遇すると、スクリプトの実行が完了するまで一時中断されます。
  3. JavaScript は、DOM と CSSOM のクエリと変更を行うことができます。
  4. JavaScript の実行は、CSSOM の準備が整うまで、遅延されます。

「クリティカル レンダリング パスの最適化」とは、大部分において、HTML、CSS、JavaScript の依存関係図を把握し、最適化することを意味します。

パーサー ブロックと非同期 JavaScript

デフォルトでは、JavaScript の実行は「パーサー ブロック」です。ブラウザがドキュメント内でスクリプトに遭遇すると、DOM 構築を一時中断して、JavaScript ランタイムに制御権を渡し、DOM 構築を進める前にスクリプトが実行されるようにします。このことは、上記のサンプルのインライン スクリプトでも確認できます。インライン スクリプトは、特別な注意を払い、追加コードを記述して実行を遅らせることがなければ、常にパーサー ブロックとなります。

script タグを通じて組み込まれたスクリプトの場合は、どうなるでしょうか。上記のサンプルを利用して、コードを個別ファイルに抽出してみましょう。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script External</title>
  </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

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

インライン JavaScript コードの代わりに <script> タグを使用した場合、実行順序に違いは生じるでしょうか。もちろん、答えは「いいえ」で、まったく同じように処理されます。どちらの場合でも、ブラウザが一時中断したあと、スクリプトが実行され、その後でドキュメントの残りの部分が処理されます。ただし、外部 JavaScript ファイルの場合、ブラウザが一時中断した後、ディスクやキャッシュ、リモート サーバーからスクリプトが取得されるのを待つ必要があります。その結果、クリティカル レンダリング パスには、ミリ秒レベルとしてはかなりの量の遅延が追加されることになります。

ただし、回避策はあります。デフォルトでは、すべての JavaScript がパーサー ブロックで、ブラウザは、スクリプトがページで何をする予定なのか知りません。そのため、最悪のケースを想定して、パーサーをブロックします。しかし、ブラウザに対して、「ドキュメント内で参照されている場所でスクリプトを実行する必要はない」と合図を伝えることができれば状況は変わります。そのようにすれば、ブラウザが DOM 構築を継続しつつ、キャッシュやリモート サーバーからスクリプト ファイルが取得されて準備が整った段階でスクリプトを実行させることができます。

この巧みな方法は、どのようにすれば実現できるでしょうか。非常に簡単です。スクリプトを async としてマーキングすればいいのです。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script Async</title>
  </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>

async キーワードを script タグに追加することで、ブラウザに対して、「スクリプトの準備が整うのを待つ間、DOM 構築をブロックしないように」と伝えることができます。この方法は、パフォーマンスの大幅な改善につながります。