Javascript developers commonly need to find out the position of an element in relation to other elements. For example, if there is a list of elements that need to be reordered, the position of the element being reordered needs to be found and updated. It turns out there are quite a few ways to go about doing this, especially when using a library with a ton of helper functions. Let’s take a look at 3 common ways of approaching this with prototype.js to see how they compare in ease of use and speed of execution.
The Experiment
The markup is setup as a standard unordered list, and within the list is a target node. For this test, the target node is identified as a class name. In real usage examples, this could be a class name, id, innerHTML
property, and so on.
<ul id="lis">
<li>1</li>
<li>2</li>
<li id="me" class="target">3</li>
</ul>
The experiment example is constructed of 800 list elements, and the results should be the position of the target list in relation to the other list elements. For example, the code above should result in 3 being returned.
Standard Loop
The first approach that comes to mind is to loop through all child nodes until a child with a class name of target
is found. Once found, returning the index of the loop will represent the position of the element.
var el = $('lis').immediateDescendants();
var count = el.length;
var ret = -1;
for(var i = 0; i < count; i++) {
if(el[i].hasClassName('target')) ret = i;
}
The Find Method
Similar to the standard approach, this approach utilizes the convenience of the prototype find
method.
var el = $('lis');
var ret = -1;
el.immediateDescendants().find(function(num, index) {
if(num.hasClassName('target')) ret = index;
});
Arguably, this approach is easier to read, and will most likely be more consistent with the rest of your code if your a fan of prototype.
Previous Siblings
By counting the amount of previous siblings an element has, the relative position is revealed. If anyone else out there has a thought process similar to mine, this approach is not the first approach taken. But it turns out the code is quite clean and efficient.
var el = $('me');
var ret = el.previousSiblings().length;
It is important to note that the use of this method is dependent on having a handle on the object. This can be done be accessing the this
object on an event triggered by the element, or by targeting it by its id
or a similar property. I have found that I often do have a handle on the element, which is why I have included this approach in the tests.
The Results
Each of the tests above were run in Safari 3, Firefox 2.0.0.5, IE6 and IE7. The speed results are below (in seconds):
Safari 3 | Firefox 2.0.0.5 | IE6 | IE7 | |
Loop | 0.064 | 0.164 | 1.522 | 1.105 |
Find | 0.068 | 0.178 | 1.532 | 1.131 |
Siblings | 0.018 | 0.076 | 1.402 | 0.995 |
So what can we take from this other than the fact that Safari 3 is super fast and IE6 is super slow? Well, the previous siblings approach is the best approach in my opinion. Obviously, it is rare to have 800 elements in a list, but the clean code and the potentially recognizable speed benefits are worth making this approach the one to default to. Between looping and finding, I would go with finding just for code consistency. Even though it is slower, the speed difference will never be noticed by a user.
Those benchmarks suggest to me that we haven’t fully optimized methods like
previousSiblings
. Such methods take an optional CSS-selector argument to filter with, but when the argument is omitted there shouldn’t be a Selector query at all. (Were these benchmarks using the latest version of Prototype?)Maybe IE is just really slow with this sort of task — your other approaches yield nearly identical results — but when I see such a disparity between IE and Firefox/Safari I immediately suspect a Selector query. I’ll look into this.
They were done with version 1.5.1.1.
Good guide for me to loop list. but my company don’t allow to use Prototype librayr :(
What tool did you use to test the time?
Also wouldn’t the spec’s of the machines (memory, processor) dictate the speed?
The Firefox and Safari 3 tests were done on the same machine. Then, the Safari 3 test was redone on a Windows machine, and the speed was very similar. That machine then ran the IE tests. It might not be 100% accurate, but I would imagine IE6 is still much slower at these types of operations.
It would be interesting to see if it makes a difference where the target is in the list. Would the previous siblings be slower if the target element was at the end of the list (#800) as opposed to early in the list?
The test was actually run with the target being #800. I haven’t done any official testing, but I believe that previous siblings is always faster.
Hey Ryan, I’m joining the discussion a little late, but this seemed worth pointing out. Don’t the hasClassName and previousSiblings methods call Element.extend on the node? Apparently, that’s a LOT slower in IE than other browsers, and the source of a many IE/Prototype bottlenecks: http://groups.google.com/group/prototype-core/browse_thread/thread/aecbf8e008782d6a
It seriously adds up over 800 iterations. I love prototype as much as the next guy, but try a vanilla comparison in your loop (e.g. if (elt.className==’target’) …). Not only will this be faster in all browsers but — most importantly — IE does it almost as fast as Firefox!
If you wanted to be a little fancier, maybe you could do $(‘lis’).getElementsByTagName(‘li’), and extend ONLY the resulting array — rather than every single node in the array. Then you can use prototype’s Array.indexOf method.