自分でこう書きながら、実は首を傾げていたのだけどやっとわかった。

404 Blog Not Found:WEB+DB PRESS vol.35
pp.57
まず速度ですが、innerHTMLは代入時にHTMLの構文解析が入るので、速度的にはDOM操作が有利です。

期待に反してそうでないのは、404 Blog Not Found:javascript - DOM vs innerHTML benchmark on MacBook Proでの指摘した通り。このあたりはamachangにちゃんと査読してもらった方がよかったのではないか?

InnerHTMLは速くない。速く見えるだけだ。

その証拠として、以下を見て欲しい。

<script type="text/javascript">
//<![CDATA[
function benchmark(button, countid){
    function $(id){ return document.getElementById(id) }
    function now(){ return (new Date).getTime() }
    var canvas  = $('canvas');
    var count   = $('count').value;
    var started = 0; 
    var counter = 0;
    var tid     = 0;
    var delay   = 0;
    var innerHTML = canvas.innerHTML = $('html').value;
    var dom       = [];document.createDocumentFragment();
    for (var i = 0; i < canvas.childNodes.length; i++){
        dom[i] = canvas.childNodes[i].cloneNode(true);
    }
    var run = {
        innerHTML : function(){    
            canvas.innerHTML = counter % 2  ? '' : innerHTML;
        },
        DOM        : function(){
            if  ( counter % 2 ){
                while(canvas.firstChild) canvas.removeChild(canvas.firstChild);
            }else{
                for (var i = 0; i < dom.length; i++) canvas.appendChild(dom[i]);
            }
        }
    }[button.value];
    var repeater = function(){
        run();
        if (++counter == count){
            window.clearTimeout(tid);
            var elapsed =  (now() - started) 
            $('log').innerHTML +=  
                button.value + '\t'  + elapsed + '\tms'
                + '\t' + elapsed/count + '\tms/op<br>\n';
            button.disabled = false;
        }else{
            tid = window.setTimeout(repeater, delay);
        }
    };
    started = now();
    button.disabled = true;
    tid = window.setTimeout(repeater, delay);
}
//]]>
</script>
<div style="border: dotted 1px; padding: 0.5em">
<textarea id="html" cols="64" rows="8">
Dan Kogai's Home Page is at
<a href="http://www.dan.co.jp/">http://www.dan.co.jp/</a>.
</textarea>
<div id="canvas"></div>
<input id="count" type="text" value="100">回
<input type="submit" value="innerHTML" onclick="benchmark(this, 'count')">
<input type="submit" value="DOM"       onclick="benchmark(this, 'count')">
<div id="log" style="font-family: monospace"></div>
</div>

このように、実際に結果をいちいち表示させるようにした場合、DOM操作とinnerHTMLでほとんど差が出ない。 少なくとも10倍以上差が出ることはありえない。

それでは、innerHTMLは高速に見えるのか?

ここからは憶測になるが、まず外していないと思う。

ヒントは、

IT戦記 - はじめての雑誌><
「イベント駆動な DOM」と「エフェクト」と「パフォーマンス」について書きました!

WEB+DB Press vol. 35 p.64
イベントには「DOMイベント」「タイマイベント」「XMLHttpRequestイベント」の3つの種類があり

記事には書かれていないが、実はもう一つ「ブラウザ内部イベント」というものがタイマーイベントとして存在すると考えるとつじつまが合う。このイベントはブラウザは定期的、おそらく画面の書き換え速度である10-30msごとに実行される。

これはこんな仕組みになっていると考えられる。pseudoscriptで書くとこんな感じだろうか

if (document.changedNodes){
  for (var i = 0; i < document.changedNodes.length; i++){
    var node = document.changedNodes[i];
    if (node.innerHTML){
       // 実際の node.childNodes は immutableなのでこうは書けない
       node.childNodes = node.innerHTML.str2nodes();
    }
  }
  document.redraw();
}

だから、実際にinnerHTMLを書き換えても、それだけではブラウザーは上の隠しプロパティchangedNodesに文字列を登録するだけで何もしない。実際にそれがDOM Treeなって再描画されるのは「ブラウザ内部イベント」発生時だけなのだ。

ゆえに、innerHTML自体は文字列を操作するだけであり、DOM Treeそのものをいじることに比べたら「その場」で払うコストはずっと安い。しかし、ブラウザ内部イベントの時に、未払い分をしっかり取り立てるわけだ。

だから、パフォーマンス向上の本当の秘訣は、innerHTMLを使うか否かではなく、いかにイベント数を減らせるかということになる。

少し深く考えればわかることだったが、10倍以上の差に目がくらんでいたようだ。反省。

しかしそれでもなおinnerHTMLと比べてdom操作まわりは使いづらい。いちいち長いしcamelizeされたmethod namesは読みづらいし書きづらいし、なぜ appendChild() だの removeChild() だので一つづつやらなければならないかわからない。多分循環参照を防ぐための配慮だと思うのだけど、めんどいったらありゃしない....

Dan the Man with Too Many Browsers to Browse