Jul 07, 2008
僕があまりDocumentFragmentを使っていない理由
自分にはちょっと承伏できない理由でJintrick氏にDISられてるので、一応釈明しておきます。
――「一応」と書いておきながら、Jintrick氏に「バカじゃねーの」みたいに煽られたような気がして感情的になってクドクド書き過ぎてしまったので、最初に例だけ示しておきます。
<?xml version="1.0"?>
<html xml:lang="ja">
<head>
<title>テスト</title>
</head>
<body onload="init()">
<script type="application/x-javascript">
function init()
{
var ul = document.getElementById('indicator');
document.addEventListener('keypress', function(aEvent) {
var li = document.createElement('li');
li.appendChild(
document.createTextNode(
[
aEvent.type,
aEvent.keyCode,
String.fromCharCode(aEvent.charCode) +'('+aEvent.charCode+')',
aEvent.target,
aEvent.target.localName,
(new Date()).getTime()
].join(' : ')
)
);
ul.insertBefore(li, ul.firstChild);
}, false);
}
</script>
<textarea rows="5" cols="50">ここに文字を入力すると、イベントの詳細を表示します。</textarea>
<ul id="indicator" style="height: 15em; overflow: auto;">
</ul>
</body>
</html>
これが正常に動作しなくなるというただそれだけの理由で、僕はJintrick氏の勧める方法(上位のDOMノードをゴソッとDOMツリーから切り離して、ツリーに対する処理を行い、最後に再挿入することで、パフォーマンスを向上する)を全面的には採用できません。
以下、長々とその説明。
ノードの挿入や削除の回数を減らしたり再描画の回数を減らしたりする方が速度上圧倒的に有利だというのは自分も理解していて、そもそもそのことを知ったのはJintrick氏が書かれた過去のドキュメントを読んでのことですし、自分もできるかぎりはDocumentFragmentを使うなどしてパフォーマンス向上に努めるようにしています。
ただ、どんな場合でも上位のDOMノードをゴソッと差し替えて高速化する方法を使うべき、とは言えません。というか、DOMツリーに変更を加えること自体が多大なリスクを伴います。
MozillaではXBLによるバインディングを使っている箇所が多くありますし、アドオンでも使っている物がありますが、DocumentFragmentによるサブツリーの差し替えを行うとこのXBLを使った部分がマトモに動かなくなる場合があります。再挿入の度に初期化処理が行われますし、DOMツリーから取り除かれる前に追加されていた匿名内容(tabbrowser要素におけるtab要素、ツリー型タブが挿入する子タブの数のインジケータなど)も失われます。
また、スクリプトであれこれしている部分を不用意に差し替えると、動かなくなったり、メモリリークの原因になったりする場合もあります(メモリリークについてはFirefox 3では大丈夫かもしれない。未検証)。冒頭のような例は、ul要素を一旦DOMツリーから取り除いて再度挿入するだけで、全く動かなくなります。もちろんこの例であれば、毎回ul要素を取得するようにすればいいのは自明ですが、自分が管理していないページでそのように変更させる事はできません。また、「パフォーマンス向上のために」敢えてそうしている人に、拡張機能のためにパフォーマンスの悪いやり方に変えろと要求する事もできません。
明らかにページ作者の方に非があるとしても、それでもきちんと動作する事が求められるのが、ブラウザとか拡張機能とかのUA(として振る舞うソフトウェア)です。ですので、より「安全」な方に倒そうと思うと、ドキュメントの内容をゴソッと切り取る方法は使いたくても使えず、パフォーマンスが悪くても必要最小限の変更を一カ所ずつ加えていかざるを得ない、というわけです。パフォーマンスが低下する事と、そのWebサービスの機能や自分が使っている他の拡張機能が動かなくなる事の、どっちがよりユーザにとって致命的か、という事を判断した結果です。そう考える自分にとって、どんな場合でもとりあえずDOMツリー丸ごと抜き出して高速化しちまえ、というのは正気の沙汰とは到底思えません。
単に手抜きで使ってない事もありますが、自分がDocumentFragmentを使った一括挿入や一括処理を用いているケースについては、「99%ほぼ確実に、その使い方で問題が起こらないと言える場合」のみです。どーしても必要じゃない事、つまり、余計な事をして、余計なリスクを高めるのは嫌なんです。「多少遅くてもちゃんと目的の処理が達成されている事」が自分にとっての「リリースしていい」と思える判断基準(バグの見落としがあるのはともかく)で、パフォーマンス向上の優先度はワリと低いです。
ちなみに、innerHTMLをあまり使っていないのも同様の理由によるところが大きいです。文字列として処理するのがやってらんねえから、というだけではなくて。
「ここでもこの高速化手法を使って問題ない」と言えるケースで自分が見落としている物(前述の「手抜き」ですね)については「確かにその通りです。すみません。」と謝って速攻で対処するしかないですが、そうでない場合については理由があってそうしているので、そこの所をちゃんと分かった上で批判して欲しいなと思います。
Documentからサブツリーを取り除くだけで色々悪影響が起こるMozillaのバインディング関係の設計がクソだとか、getElementByIdで取った参照がライブでないというDOMの設計(実装?)がクソだとか、そういう批判ももちろんアリですけど、それはMozillaの方に言って欲しいです。僕に言われても困ります。
自分が知らない範囲の事については大して気にしないのに、知っている範囲の事については過剰に気にする、そのダブスタ姿勢が気に食わん、と言われたら、まあ、そういう性格なので……と言うしかないですね。
リンク先の文書を最初見た時「ああ、これは暗に僕の事を言ってるんだろうなあ」と分かってはいましたが、どんな環境で動作するか分からない処理を書くという立場で考えれば、「Jintrick氏のおっしゃる事はあまりに危険すぎて選択できない」というのは自明すぎてわざわざ釈明するほどの事も無いだろう、と思っていました。でも7日付の変更でそういう点を無視されて名指しで非難されたので、こちらもきちんと理由を述べて釈明しておこうと思った次第です。
なお、タイトルで「DocumentFragment」と書いていることからDocumentFragmentそのものを悪者扱いしているように読んでしまわれる方がいるかもしれませんが、それは誤解です。DocumentFragmentは複数のノードを同一階層に一気に挿入するために利用できるなど大変有用な物ですし、絶対に安全と言い切れる場合であればリンク先文書のような使い方も全然アリです。問題は、元のDOMツリーからノードを切り離すことで失われてしまう物があるので、ノードを切り離すことには慎重にならないといけない場合がある、ということです。
余談ですが、リンク先にも書かれている通り、文字列連結について「+」演算子で結合するよりArrayのjoinを使った方が高速だという話をよく耳にしますけれども、ことFirefox上で動作するJavaScriptの場合は、あまりアテにならない話のようです。以下のようなテストケースを実行した所、確かにArrayを使った方が若干高速であはるようでした(うちの環境だとArrayの方が3%高速という結果でした)が、数万回程度の処理ではビックリするような速度差は出ないようですので、それより圧倒的に回数が少ないよくある場合はどっちでも好きな方を使えばいいんじゃね?と思います。
var plusTimes = 0;
var words = ['a', 'abc', 'abcd'];
var roop = 50000;
for (var j = 0, maxj = 10; j < maxj; j++)
{
var start = new Date();
var result = '';
for (var i = 0, maxi = roop; i < maxi; i++)
{
result += String.fromCharCode(parseInt(Math.random() * 26)) +
words[parseInt(Math.random() * 3)];
}
var end = new Date();
plusTimes += (end - start);
}
alert((plusTimes / 10) + 'msec');
var arrayTimes = 0;
for (var j = 0, maxj = 10; j < maxj; j++)
{
var start = new Date();
var result = [];
for (var i = 0, maxi = roop; i < maxi; i++)
{
result[i+1] = String.fromCharCode(parseInt(Math.random() * 26)) +
words[parseInt(Math.random() * 3)];
}
result = result.join('');
var end = new Date();
arrayTimes += (end - start);
}
alert((arrayTimes / 10) + 'msec');
alert('array is faster than "+" '+
Math.round((plusTimes / arrayTimes) * 100 - 100)+'%');
もっと膨大な処理をする場合なら差は大きくなるでしょうが、そういう場合では言語としてJavaScriptを使うこと自体考え直した方がいいでしょうね。この例を自分とこで実行すると、それぞれ20秒近く固まりましたので(固まらないようにタイマーを使う等したら、関数呼び出しのオーバーヘッドが大きすぎて元の木阿弥になります)。
wikieditish message: Ready to edit this entry.