jQuery の ID で対象ノードを取得する処理を高速にしたい

てっく煮ブログさんのエントリー
jQuery を高速に使う CSS セレクタの書き方 - てっく煮ブログ
をみてて、jQuery の $("#xxxx") を速くしたいと考えていたことを思いだした。それについて書く。

jQuery の $("#xxxx") について

セレクタをどう処理しているのか jquery-1.2.6.js で確認してみよう。
まず下記の init が $() に相当する。Handle HTML stringsのところで引数が string だと quickExpr.exec が走る。これは正規表現でのセレクタ解析処理である。
その後、HANDLE: $("#id")のところでマッチして取得した ID を使って、var elem = document.getElementById 使い、それを return jQuery(elem) として再び init が呼び出される。

jQuery.fn = jQuery.prototype = {
	init: function( selector, context ) {
		// Make sure that a selection was provided
		selector = selector || document;

		// Handle $(DOMElement)
		if ( selector.nodeType ) {
			this[0] = selector;
			this.length = 1;
			return this;
		}
		// Handle HTML strings
		if ( typeof selector == "string" ) {
			// Are we dealing with HTML string or an ID?
			var match = quickExpr.exec( selector );

			// Verify a match, and that no context was specified for #id
			if ( match && (match[1] || !context) ) {

				// HANDLE: $(html) -> $(array)
				if ( match[1] )
					selector = jQuery.clean( [ match[1] ], context );

				// HANDLE: $("#id")
				else {
					var elem = document.getElementById( match[3] );

					// Make sure an element was located
					if ( elem ){
						// Handle the case where IE and Opera return items
						// by name instead of ID
						if ( elem.id != match[3] )
							return jQuery().find( selector );

						// Otherwise, we inject the element directly into the jQuery object
						return jQuery( elem );
					}
					selector = [];
				}

			// HANDLE: $(expr, [context])
			// (which is just equivalent to: $(content).find(expr)
			} else
				return jQuery( context ).find( selector );

		// HANDLE: $(function)
		// Shortcut for document ready
		} else if ( jQuery.isFunction( selector ) )
			return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );

		return this.setArray(jQuery.makeArray(selector));
	},

よって、コストがかかっているのは正規表現の解析部分のようなのでこの部分を飛ばして、いきなり jQuery に DOM ノードを渡してあげると速くなりそうだ。

高速化方法とまとめ

IDによる jQuery オブジェクトの取得を下記のように定義しておくと簡単に速くできそうだ。($$ にしたのは prototype.js が $("xxx") で取れたりそれっぽいから)

function $$(id) {
  return $(document.getElementById(id));
}

(追記)上記の $$ の定義では、指定した ID が DOM ツリーに存在しないときの処理を考慮していない。

Windows Vista SP1 での計測実験 / 1000回実行の計測三回平均

IE 7.0Firefox 3.0Opera 9.6Safari 3.2
jQuery $("#id")707 ms301 ms157 ms129 ms
document.getElementById("id")201 ms43 ms35 ms21 ms
jQuery $$("id")470 ms126 ms53 ms51 ms
document.getElementById は参考値としてです。いずれもブラウザでも $$ による取得の方が速くなっていることがわかる。ただ、IE では他のブラウザほど速くならなかったのが残念だ。

セレクタで ID を指定して取得を行うことはよくある。アプリの規模によってはこういった高速化を施しておくのも良いだろう。

テスト用のHTML

上記の計測に使った HTML ソースをべた貼りしておく。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="content-script-type" content="text/javascript">
<title>jQuery getElementById() の計測テスト</title>
<script type="text/javascript" src="jquery-1.2.6.js"></script>
<script type="text/javascript">
function $$(id) {
  return $(document.getElementById(id));
}

function printResult(id, ms){
  var p = document.createElement('p');
  p.innerHTML = id + " : " + ms + "ms";
  document.getElementById(id).appendChild(p);
}

function runTest(testfunc, count){
  var s1 = new Date().getTime();
  for (var i = 0; i < count; i++)
    testfunc();
  return new Date().getTime() - s1;
}

function getCount(){
  return document.getElementById('textCount').value + 0;
}

function testJquery(count){
  var funcA = function(){
      var d = $("#dummy");
    };
  
  printResult('jQuery', runTest(funcA, count) );
}

function testGetElementById(count){
  var funcA = function(){
      var d = document.getElementById("dummy");
    };
  
  printResult('getElementById', runTest(funcA, count) );
}


function $$(id) {
  return $(document.getElementById(id));
}


function test$$(count){
  var funcA = function(){
      var d = $$("dummy");
    };
  
  printResult('newGetElementById', runTest(funcA, count) );
}

function newGetElementByIdTest(){
  
  alert($$("dummy").find(".best").text());
}


</script>
<style type="text/css">
div { border-top:1px solid #666; margin:10px 0; padding:5px; }
</style>
</head>
<body>
<div><p>テスト回数 : <input type="text" id="textCount" value="1000"/></p></div>
<div><p id="dummy">テストノード <span class="best">取得テスト用</span> dummy</p></div>
<div id="jQuery">
<pre>
var d = $("#dummy");
</pre>
 <p><button onclick="testJquery(getCount())">jQuery 判定</button></p>
</div>
<div id="getElementById">
<pre>
var d = document.getElementById("dummy");
</pre>
 <p><button onclick="testGetElementById(getCount())">getElementById 判定</button></p>
</div>
<div id="newGetElementById">
<pre>
function $$(id) {
  return $(document.getElementById(id));
}

var d = $$("dummy");
</pre>
 <p><button onclick="test$$(getCount())">$$ 判定</button></p>
</div>
<div id="newGetElementByIdTest">
<pre>
alert($$("dummy").find(".best").text());
</pre>
 <p><button onclick='alert($$("dummy").find(".best").text())'>$$ が jQuery オブジェクトか確認</button></p>
</div>
</body>
</html>

jQuery 1.3 において(2009/1/18 追記)

上記を jquery-1.3.min.js に差し替えて計測してみましたが、変わらずでした。