innerHTML += ... な書き方について

はてなブックマークで『後でツッコミする』と書いたとおり、ちょっとツッコミたいと思う。
ツッコミ記事のつもりが、自分がツッコミされることとなり、ダメダメな記事です。それでも良ければお読み下さいw

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

上記コードはダメなコードであり、理由は、

  • li要素をループが回るたびに追加しているため
  • レイアウト・レンダリングによる遅さ

というのが主な原因として挙げられています。

ボクは、このコードがダメな理由は、もちろんその理由もあるんだけど、もっとダメな理由があると思っています。

そうでないと、下記コードのような、同じようにループ中に何度もDOMツリーへの更新をするコードとのパフォーマンスの差を説明できないからです。

var ul = document.querySelector('#output');
var li;
for (var i = 0; i < data.length; i++) {
  li = document.createElement("li");
  li.textContent = data[i];
  ul.appendChild(li);
}

innerHTML += ... がやっていること

単純に ... を追加しているわけではないです。

innerHTML の Getter アクセスがある

element.innerHTML += "a"element.innerHTML = element.innerHTML + "a" と等価です。

右辺で element.innerHTML の値を得て、 + "a" するわけです。ここで、element のDOMツリーを文字列へ変換する処理があります。

innerHTML への Setter はツリーの全置換えである

element.innerHTML への代入処理は

  1. elementの子要素をクリアする
  2. 与えられた文字列を、DOMオブジェクトに変換する
  3. 変換したDOMオブジェクトを子要素として追加する

ということをしていると思われます。 += 演算子を使っていると、まるで追加処理のように見えてしまいますが、全取っ替えです。

ツッコミ先のページでは、3つ目の子要素に追加だけが原因かのように言っていますが、本当に3つ目が原因なのでしょうか? 少々疑問です。

検証

ちょいコード書き直した

document.body.insertAdjacentHTML("beforeend", '<div id="test"></div>');
var $test = document.getElementById("test");
var count = 100;

function clearDiv() {
  $test.innerHTML = "";
}
function test_1 () {
  console.time("innerHTML");
  for (var i = 0; i < count; ++i) {
    $test.innerHTML += "<span>" + i + "</span>";
  }
  console.timeEnd("innerHTML");
  clearDiv();
}

function test_2 () {
  console.time("innerHTML2");
  for (var i = 0, html = ""; i < count; ++i) {
    html += "<span>" + i + "</span>";
    $test.innerHTML = html;
  }
  console.timeEnd("innerHTML2");
  clearDiv();
}

function test_3 () {
  console.time("insertAdjacentHTML");
  for (var i = 0; i < count; ++i) {
    $test.insertAdjacentHTML("BeforeEnd", "<span>" + i + "</span>");
  }
  console.timeEnd("insertAdjacentHTML");
  clearDiv();
}

function test_4 () {
  console.time("appendChild");
  for (var i = 0, span; i < count; ++i) {
    span = document.createElement("span");
    span.textContent = i;
    $test.appendChild(span);
  }
  console.timeEnd("appendChild");
  clearDiv();
}

テキトウな検証なので、細かいところは分かりませんが、まぁ参考値ということで Firefox で実施した結果を載せておきます。

innerHTMLinnerHTML 2insertAdjacentHTMLappendChild
Time 22.41ms 18.36ms 1.48ms 1.16ms
主な処理内容
(ループ中)
  • innerHTML への getter アクセス
  • 子要素の削除
  • 文字列のDOM変換
  • DOMツリーへの追加
  • レンダリング

本当に正しい検証かどうか怪しいんだけど、innerHTML の getter が結構な遅さを誇っているのではないかなと思うわけです。getter アクセスでコストが掛かっているわけではなさそう。


以上、レンダリングに関わる部分をループ中に処理するのがダメな理由として innerHTML を使用したサンプルは悪手なんじゃない? というツッコミでした。

検証コードのミスを修正したら、自分が思ったのとは違う結果になってしまった。えーと、偉そうな事書いて、すみませんでした><