JavaScript によるインタラクティビティの追加

公開日: 2013 年 12 月 31 日

JavaScript を使用すると、コンテンツ、スタイル設定、ユーザー操作に対するレスポンスなど、ページに関するあらゆる変更を加えることができます。ただし、JavaScript は DOM の構築をブロックして、ページのレンダリングを遅らせてしまうことがあります。最適なパフォーマンスを実現するには、JavaScript を非同期にして、クリティカル レンダリング パスから不要な JavaScript をすべて取り除く必要があります。

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

JavaScript はブラウザで実行される動的な言語であり、ページの動作のほぼすべての側面を変更できます。DOM ツリーの要素を追加または削除してコンテンツを変更できます。各要素の CSSOM プロパティを変更する、ユーザー入力を処理できるなど、多くのことができます。わかりやすいように、先ほどの「Hello World」の例を変更して短いインライン スクリプトを追加するとどうなるかを見てみましょう。

<!DOCTYPE html>
<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> 要素の上に移動するとスクリプトは失敗し、ドキュメントで <span> 要素への参照が見つからないというエラーが出ます。つまり、getElementsByTagName('span')null を返します。これは重要な特性を示しています。つまり、スクリプトはドキュメントに挿入された位置で実行されます。HTML パーサーが script タグに遭遇すると、DOM 構築のプロセスを一時中断し、JavaScript エンジンに制御を渡します。JavaScript エンジンの実行が完了すると、ブラウザは中断前の位置に戻り、そこから DOM 構築を再開します。

つまり、ページの最後の方にある要素はまだ処理されていないため、スクリプト ブロックでそれらの要素を見つけることはできません。言い換えれば、インライン スクリプトの実行は、DOM 構築をブロックし、結果的に最初のレンダリングが遅れるということです。

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

スクリプトを実行する時点で、ブラウザが CSSOM のダウンロードと構築を完了していないと、どうなるでしょうか。この答えはパフォーマンスにとってあまり良くありません。ブラウザは、CSSOM のダウンロードと構築が完了するまで、スクリプトの実行と DOM 構築を遅らせます

つまり、JavaScript によって、DOM、CSSOM、JavaScript の実行において新たな依存関係が多く生まれます。これにより、ブラウザによる画面上のページの処理とレンダリングが大幅に遅れる場合があります。

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

「クリティカル レンダリング パスの最適化」とは、主に HTML、CSS、JavaScript の依存グラフを理解して最適化することを意味します。

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

JavaScript の実行はデフォルトで「パーサー ブロック」になっています。つまり、ブラウザはドキュメント内のスクリプトを検出すると DOM 構築を一時停止し、JavaScript ランタイムに制御を引き継ぎ、スクリプトを実行させてから DOM 構築を進める必要があります。前の例で、インライン スクリプトを使用してこれを実際に確認しました。事実、インライン スクリプトは、実行を遅らせる追加コードを記述しなければ必ずパーサー ブロックになります。

スクリプトタグを使用して組み込まれたスクリプトの場合は、どうでしょうか。前述のサンプルのコードを個別ファイルに抽出してみましょう。

<!DOCTYPE html>
<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 属性を <script> 要素に追加します。

<!DOCTYPE html>
<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>

試してみる

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

フィードバック