HTML5Experts.jp

DOM操作の最適化によるJavaScriptチューニング(前編)

連載「Webサイト・アプリ高速化テクニック徹底解説」の第4回は、JavaScriptのチューニングのうち、ボトルネックになりやすいDOM操作の最適化について解説します。前編・後編にわたって、DOM操作が遅くなる原因と仕組み、その解決策について詳しく解説します。

CodeIQとの連動企画!

この記事で学べるJavaScriptチューニングのテクニックを、実際にCodeIQの問題で試すことができます。もう既に自信がある方は腕試しに、もしくは理解度チェックのための復習として是非ご活用ください!こちらから問題にチャレンジ!

DOM操作の最適化によるJavaScriptチューニング(前編)

DOM(Document Object Model)とは、HTMLをアプリケーション(ここではJavaScript)から利用するためのAPIです。JavaScriptによるユーザーインターフェースの構築やレスポンスの表示など、インタラクティブな部分はほとんどがDOM操作によるものでしょう。このように、非常に利用頻度の高いDOM操作ですが、同時に性能的にボトルネックになりやすい箇所でもあります。これらのDOM操作の最適化について、今回から前編・後編にわたって詳しく解説していきます。

DOM操作は書き方によってスピードに数十倍の差が出る

DOM操作は、書き方によって非常にパフォーマンスに差が出ます。例えば、次のようなコードを見てみましょう。id属性に”output”と指定された要素に、data配列の中身を単純にリスト表示するだけのものです。

:javascript:
// サンプル1: パフォーマンスが悪い
var ul = document.querySelector('#output');
for ( var i = 0; i < data.length; i++ ) {
  ul.innerHTML += '<li>' + data[i] + '</li>';
}

これは、非常にパフォーマンスが悪い例です。これを次のように書き換えるだけで、環境にもよりますが速度は数十倍にもなります。

:javascript:
// サンプル2: サンプル1に比べてパフォーマンスが良い
var ul = document.querySelector('#output');
var html = '';
for ( var i = 0; i < data.length; i++ ) {
  html += '<li>' + data[i] + '</li>';
}
ul.innerHTML = html;

このように、ボトルネックとなっているDOM操作をチューニングすれば、パフォーマンスが大きく改善する可能性があります。ここからは、DOM操作が遅くなる原因と仕組みを解説しながら、対応策を見ていきます。

なぜDOM操作は遅いのか?

DOM操作はなぜ遅いのでしょうか。それには、大きく2つの理由があります。1つ目は、DOMの実装方法によるものと、2つ目は、DOM操作によるレンダリングのコストが非常に高いということが挙げられます。この2つの理由と対応方法を覚えておけば、さまざまなコードに応用できます。それぞれ見ていきましょう。

ブラウザの実装による遅さ

元々DOMは、特定のプログラミング言語を対象としたものではなく、別々の言語から汎用的にアクセスできるような仕様になっています。そのため、多くのブラウザでは、HTMLのレイアウトや描画を担当するレンダリングエンジンと、JavaScriptを担当するスクリプトエンジンに分かれています。このようにスクリプト部分をモジュール化することによってDOM操作に別のプラグラミング言語を使うことも容易になっています。しかしながら、そのデメリットとして、JavaScriptからDOM操作を行うということは、モジュール間をブリッジするコストが発生することにもなります。そのため、DOM操作自体がプリミティブな操作に比べて非常に低速になっています。そういった観点から、HTMLの要素の単純なプロパティ参照なども含めて、極力DOM操作を減らすようにすることが重要です。

レイアウト・レンダリングによる遅さ

DOM操作の結果は、実際のHTMLに反映される際に、ブラウザでさまざまな処理が起こります。例えば、要素が追加された場合には、それによって発生する各要素の大きさや位置といったレイアウトを他の影響する要素を含めて、すべて再計算しなければなりません。また、レイアウトの再計算が終わったあとには、それらを画面にレンダリングする必要もあります。そして、これらの処理は非常にコストが高いものです。そのため、DOM操作によってレイアウトやレンダリングに影響がある範囲、回数をなるべく減らす必要があります。

例えば、冒頭に示したサンプル1のコードでは、次のようにli要素をループが回るたびに追加しているため、レイアウトとレンダリングが何回も発生して非常に遅くなってしまう可能性があります(レイアウトやレンダリングは非同期で処理されるため、実際にはブラウザがある程度、自動的に最適化してくれます)。

:javascript:
// 複数回、レイアウト、レンダリングされる可能性がある
for ( var i = 0; i < data.length; i++ ) {
  ul.innerHTML += '<li>' + data[i] + '</li>';
}

innerHTMLを利用しているため、少なくてもHTMLのパースはループのたびに発生します。また、古いブラウザでは文字列の連結自体が問題になることもあります。このコードを、レイアウトとレンダリングが1回になるように書き換えると次のようになります。(その他にも、innerHTMLを「+=」演算子で余分に参照していることや、innerHTMLの中身を毎回すべて書き換えていることも遅さの原因になっていますが、次のコードではそれらも改善しています。)

:javascript:
// 変数にHTMLを格納しておき最後に1回だけ書き換える
var html = '';
for ( var i = 0; i < data.length; i++ ) {
  html += '<li>' + data[i] + '</li>';
}
ul.innerHTML = html;

最終的に、結果が表示されれば良いので、li要素のHTMLは、いったん別の変数に格納しておき、最後に1回だけinnerHTMLへ代入しています。これによって、DOM操作の回数もレンダリングの回数も以前のコードに比べて1回で済みます。

innerHTMLとDOM操作メソッド

ここまでのサンプルでは、innerHTMLを使ってDOM操作を記述していますが、createElement()やappendChild()などのDOM操作メソッドを使う方法もあります。次のサンプルは、サンプル2をDOM操作メソッドを利用して記述した例です。

:javascript:
// サンプル3: createElement()を使った例
var ul = document.querySelector('#output');
var fragment = document.createDocumentFragment();
for ( var i = 0; i < data.length; i++ ) {
  var li = document.createElement('li');
  li.textContent = data[i];
  fragment.appendChild(li);
}
ul.appendChild(fragment);

innerHTMLとDOM操作メソッドは、それぞれ良い点がありますが、基本的にはDOM操作メソッドを使っていくほうがクロスサイトスクリプティングの脆弱性を作りこみにくいので安全です(innerHTMLは、文字列をパースするためユーザーの入力値を使う場合はエスケープする必要があります)。これらのDOM操作メソッドを利用する方法は、次回で詳しく取り上げていきます。

innerHTMLとDOM操作メソッドのスピード

innerHTMLとcreateElement()などのDOM操作メソッドは、どちらが速いかといった形でよく比較されることが多いですが、パフォーマンスという観点からはそれほど気にしなくても良いでしょう。モダンな環境では、お互いの速度差はそれほど大きくありませんし、ブラウザは常にバージョンアップされていますのでベンチマークの結果はすぐに変わります。特定の環境に特化したチューニングを行うのでなければ、細かい速度差を気にするよりは、機能の違いで使い分けていきましょう(ちなみに、現在はIE、FirefoxはinnerHTMLの方が若干速く、Chrome、SafariはDOM操作メソッドの方が若干速くなっています)。

次回の内容について

今回は、DOM操作が遅くなる理由と、その対処方法について簡単に解説しました。次回は、createElement()によるDOM操作と、チューニングのためのさまざまなTipsを紹介していく予定です。お楽しみに!