getElementsByClassName re-re-re-visited

By crisp on Monday 13 August 2007 09:07 - Comments (3)
Category: Javascript, Views: 12.212

There have been numerous attempts at creating the most optimal getElementsByClassName implementation. I also took a shot at it back in 2005 and back then Opera had some performance problems with getElementsByTagName, IE's implementation of nodeList.item() proved to be a major hog and Firefox' XPath implementation was far from perfect. Eventually I came up with a hybrid solution that combined the fastest approaches for each tested browser (with IE being declared the winner on performance).

Fast forward to 2007 and here we are again discussing getElementsByClassName :)
But how the landscape has changed: most browsers now support XPath quite well, Gran Paradiso even has native support for getElementsByClassName and other browsers are expected to follow suit.

Also we now have quite a number of javascript libraries available that offer this functionality. Most go even beyond that and offer interfaces where you can use advanced CSS syntax to select elements: jQuery, cssQuery, $$ and the likes, so why should I even bother? And why bother with something so simple compared to all the tools that can take advantage of full CSS syntax?

The answer is just as simple: sometimes you just don't need more than that, and simple things are easier to tweak to get the utmost performance. Besides that getElementsByClassName is the mother of it all, the very basics; if you can't get that right than how about the rest?

First of all we need to decide how basic getElementsByClassName needs to be. Should it be able to take multiple classes as an argument? Should it have an argument that specifies if those multiple classes should be OR'ed or AND'ed? Should it have an argument that specifies some specific nodeName(s)? Should it have an argument for a callback-function to apply to all elements? I decided on only one of those options: to include an argument to specifiy a specific nodeName, and with one reason: it can seriously improve performance in most cases.

Now my first intention was to present some benchmarks on how my solution performs compared to a.o. prototype and YUI, but honestly that would be comparing apples to oranges.

As for prototype: version 1.5.1 does not consider any native implementation; in fact: it overwrites getElementsByClassName for document. Furthermore Element.extend() seriously hurts performance in IE. I could compare performance against $$() but that doesn't seem to be well optimized for some case either.

As for YUI: version 2.3.0 still only uses the most basic approach for all browsers; it doesn't do XPath and doesn't check for native support. It does have support for an argument to specify a specific nodeName to go with the class but because it doesn't check for availability of faster methods it is lagging in performance in most browsers.

So without further ado I'm just presenting the code here, hoping it will be of some inspiration to library-makers:

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var getElementsByClassName = function()
{
    // native
    if (document.getElementsByClassName)
    {
        return function(classNamenodeNameparentElement)
        {
            var s = (parentElement || document).getElementsByClassName(className);

            if (nodeName && nodeName != '*')
            {
                nodeName = nodeName.toUpperCase();
                return Array.filter(sfunction(el) { return el.nodeName == nodeName; });
            }
            else
                return [].slice.call(s0);
        }
    }

    // xpath
    if (document.evaluate)
    {
        return  function(classNamenodeNameparentElement)
        {
            if (!nodeNamenodeName = '*';
            if (!parentElementparentElement = document;

            var results = [], si = 0element;

            s = document.evaluate(
                ".//" + nodeName + "[contains(concat(' ', @class, ' '), ' " + className + " ')]",
                parentElement,
                null,
                XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
                null
            );

            while ((element = s.snapshotItem(i++)))
                results.push(element);

            return results;
        }
    }

    // generic
    return function(classNamenodeNameparentElement)
    {
        if (!nodeNamenodeName = '*';
        if (!parentElementparentElement = document;

        var results = [], si = 0element;
        var re = new RegExp('(^|\\s)' + className + '(\\s|$)'), elementClassName;

        s = parentElement.getElementsByTagName(nodeName);

        while ((element = s[i++]))
        {
            if (    (elementClassName = element.className) &&
                (elementClassName == className || re.test(elementClassName))
            )
                results.push(element);
        }

        return results;
    }
}();

Some notes though:
  1. I implemented this as a global function where parentElement is an optional argument rather than the scope of some method-call
  2. I assume HTML by uppercasing nodeName. I wonder what would be the best test for XHTML?
  3. I use the Array:filter method as a generic - I'll explain in an upcoming post how to implement those in other browsers.
  4. I also use some trickery to convert the live nodeList returned from a native getElementsByClassName implementation to a plain array (as to match what is returned by the other alternatives), I'll go into detail about that trick as well when I post about 3)
As for winners and losers when it comes to browser-performance: last time I declared IE the winner in performance and when only using the generic approach (as does YUI) it still is. However when you take into account the alternative approaches using XPath or native implementation IE now proves to be the biggest loser and Gran Paradiso the biggest winner :)

Volgende: Crossbrowser Array Generics 08-'07 Crossbrowser Array Generics
Volgende: prototype: IE and the cost of Element.extend() 08-'07 prototype: IE and the cost of Element.extend()

Comments


By Tweakers user NitroX infinity, Monday 17 December 2007 22:15

The prototype link is busted.

Nice read btw :)

By Tweakers user XWB, Monday 31 December 2007 16:43

Grappig detail trouwens wat prototype betreft: http://www.prototypejs.or...nt/getElementsByClassName
As of Prototype 1.6, document.getElementsByClassName has been deprecated since native implementations return a NodeList rather than an Array. Please use $$ or Element#select instead.
:)

By Tweakers user crisp, Friday 11 January 2008 12:40

Hacku: ja, dat was nav een bugreport van mijzelf ;)

Nadeel van $$ (en Element#select) is dat het vele malen trager is, zeker bij browsers die nu native getElementsByClassName ondersteuning bieden en waarbij je simpel Array.slice(nodeList, 0) kan doen om de nodeList om te zetten naar een static array :)

Comments are closed