![]()
document.getElementById() が非効率で使えない
という指摘を時々見かけるが、その実装や計測結果をみるとモダンブラウザーには当てはまらないと思う。ただ Internet Explorer 8 以前で少なくとも 100 回以上 getElementById() を呼び出すような場合はパフォーマンスを考慮する必要があるかも。
※ JavaScript プログラマーではないので思い込みや勘違いがあるかもしれません。適切でない記述などはコメントでお知らせください。
AnimateImage のおかげで体系的に学んでもいないのに JavaScript を書いている近頃。Google の助言 (検索で得た情報) を参考にしていると document.getElementById() に対して次のような指摘を見かけることがある。
getElementById() は DOM ツリーを全探索して指定した id を持つ要素を返すため非常に効率が悪い。getElementById() を何度も呼ぶ必要がある場合は次のように実装すべき。
- getElementById() の戻り値 (element) をキャッシュする
- getElementById() を使わずに次のように実装する
- コレクションプロパティ (anchors, forms, images, links など) を参照する
- getElementsByTagName() メソッドを利用する
- 手動で直接 DOM ツリーにアクセスする
もっともな指摘のようにもみえるが、少し考えてみると getElementById() がそんなイケてない実装とはとても思えない。戻り値のキャッシュで改善するのであれば API 側でハッシュマップを使用しているはず。
※ getElementById() の仕様によれば厳密にはブラウザーの実装依存。だけどメソッドを呼び出すたびにツリーを全探索するような実装は、コードレビュアーに呼び出されるかパフォーマンスバグ扱いされるレベル。
ということで document.getElementById() がどのように実装されているか実際に調べてみた。
結論から言えば、オープンソースで確認できる範囲では getElementById() はすべてハッシュマップを使用して実装されていることが判明。
以上の点から Firefox ではハッシュテーブル (ハッシュマップ) を使用して getElementById() が実装されていることが分かる。
Element*
nsDocument::GetElementById(const nsAString& aElementId)
{
if (!CheckGetElementByIdArg(aElementId)) {
return nsnull;
}
nsIdentifierMapEntry *entry = mIdentifierMap.GetEntry(aElementId);
return entry ? entry->GetIdElement() : nsnull;
}
nsTHashtable<nsIdentifierMapEntry> mIdentifierMap;
以上の点から Chrome と Safari でもハッシュマップを使用して getElementById() が実装されていることが分かる。
Element* Document::getElementById(const AtomicString& id) const
{
return TreeScope::getElementById(id);
}
Element* TreeScope::getElementById(const AtomicString& elementId) const
{
if (elementId.isEmpty())
return 0;
return m_elementsById.getElementById(elementId.impl(), this);
}
private:
DocumentOrderedMap m_elementsById;
org.w3c.dom.Document を implements している com.sun.org.apache.xerces.internal.dom.CoreDocumentImpl でもハッシュテーブル (ハッシュマップ) を使用して getElementById() が実装されている。
protected Hashtable identifiers;
public Element getElementById(String elementId) {
return getIdentifier(elementId);
}
public Element getIdentifier(String idName) {
if (needsSyncData()) {
synchronizeData();
}
if (identifiers == null) {
return null;
}
Element elem = (Element) identifiers.get(idName);
if (elem != null) {
// check that the element is in the tree
Node parent = elem.getParentNode();
while (parent != null) {
if (parent == this) {
return elem;
}
parent = parent.getParentNode();
}
}
return null;
} // getIdentifier(String):Element
オープンソースでないブラウザーは getElementById() の実装が分からないため呼び出しコストも不明。そこで実際にパフォーマンス計測してみることにした。
テストケースの内容は次の通り。詳細はソースを参照。
| Web ブラウザー | getElementById() | プラットフォーム |
|---|---|---|
| Mozilla Firefox 5.0 | 0.0006 ms | Windows XP |
| Google Chrome 12.0 | 0.0008 ms | Windows XP |
| Apple Safari 5.0.5 | 0.0019 ms | Windows XP |
| Opera 11.50 | 0.0032 ms | Windows XP |
| Internet Explorer 10* | 0.0077 ms | Windows 7 (仮想マシン) |
| Internet Explorer 9 | 0.0074 ms | Windows 7 (仮想マシン) |
| Internet Explorer 8 | 1.82 ms | Windows XP |
| Internet Explorer 7 | 4.77 ms | Windows XP (IETester) |
| Internet Explorer 6 | 5.59 ms | Windows XP (IETester) |
| Internet Explorer 5.5 | 4.29 ms | Windows XP (IETester) |
[*] Internet Explorer 10 Test Drive (Platform Preview 2.10)
以上の結果から Internet Explorer 8 以前は getElementById() のパフォーマンスが桁違いに低いことが分かる。ただ一番遅い Internet Explorer 6 でも 100 回呼び出して 0.5 秒なので、ソースの可読性や保守性を考えると無条件に getElementById() を使うべきでないとは断言できないと思う。
※ 計測値は getElementById() の 1 回あたりの呼び出し時間で、何回か試行した結果の平均値か収束値となっている。計測値は次のような項目により増減するため絶対値ではなく相対的に評価する必要がある。
getElementById() のパフォーマンスに問題がないからといって、明らかに無駄な呼び出しをしても構わない理由にはならない。たとえば次のような実装 (同じ要素をループ内で取得) は JavaScript に限らずかなり恥ずかしいコード。
for (var i = 0; i < count; i++) {
var elem = document.getElementById('foo');
// elem に対する処理
}
コメント