jQuery has changed the way we write Javascript by abstracting out much of the painful cross-browser implementation details that used to plague developers, but to use it correctly still requires a little knowledge about what’s going on under the hood. In this post we’ll take a good look at jQuery’s selectors and how to use them efficiently. I’ll also talk briefly about DOM manipulation and event handlers.
Part 1: Search
At its core jQuery is exactly what its name implies, a query engine designed for search. And just like you’re careful to construct efficient SQL queries, you need to take the same care with your jQuery selectors. Efficient selector use boils down to three main concepts.
- Using the right selector
- Narrowing the search space
- Caching
Ego and #ID
You’re right, it’s never that clear cut
Good browsers actually provide a getElementsByClass which greatly improves the performance of class selectors. This can lead to surprises later when you start testing in IE, while your code will run quickly in Firefox/Webkit, IE will slow to a crawl because jQuery has to emulate getElementsByClass in Javascript.
There is a clear hierarchy of selectors when it comes to speed, from fastest to slowest this is:
- ID selectors (“#myId”)
- element selectors (“form”, “input”, etc.)
- Class selectors (“.myClass”)
- Pseudo & Attribute selectors (“:visible, :hidden, [attribute=value]”)
ID and element selectors are fastest because they are backed by native DOM operations, specifically getElementById()
and getElementsByTagName()
. Class selectors along with Psuedo and Attribute selectors have no browser-based call to leverage which puts them at a distinct disadvantage. We’ll see how to take advantage of the speed of #ID selectors in a bit, but first a note on “pseudo selectors”.
Pseudo selectors provide a lot of power in the right situations but they’re also a lot slower. To understand why let’s take a look at how :hidden
works in jQuery 1.4.2.
jQuery.expr.filters.hidden = function( elem ) { var width = elem.offsetWidth, height = elem.offsetHeight, skip = elem.nodeName.toLowerCase() === "tr"; return width === 0 && height === 0 && !skip ? true :width > 0 && height > 0 && !skip ? false :jQuery.curCSS(elem, "display") === "none"; };
See? The magic is revealed, :hidden
is actually a function that must be run against every element in your search space. If you have a page with 1000 elements and you call $(":hidden")
you’re actually asking jQuery to call the above function a thousand times. You can get away with this if the number of elements you’re iterating over is small (you did root your selector with an ID, right?) but it’s important to keep in mind that you are asking jQuery to run a function against your elements when using :pseudo
style selectors.
Narrowing the search
Good semantics, good selectors
In writing your HTML remember that the purpose of IDs are to identify singular elements in your page. Generally you want IDs if you’re trying to tag a single element, and a class only if you’re tagging a collection of related elements. Good HTML semantics go hand-in-hand with optimal selector use.
To reduce our search time we need to reduce the search space. Because the DOM itself is a tree structure we accomplish this by rooting our search in a sub-tree.
$(".myClass")
will search every element in the DOM for your class. In contrast $("#myId .myClass")
will only search the elements within the sub-tree rooted at #myId
. In large pages this can be the difference between searching tens of elements over hundreds or thousands.getElementsByTagName()
under the hood to reduce the search space before proceeding. In fact, it’s rare that you need to refer to a bare class selector and you should avoid it when possible.find()
and filter()
to narrow your search even more. These can be useful when you’ve cached a selector but would like to use only a specific subset of its elements.
Cache Rules Everything Around Me
You may have noticed that the jQuery syntax we all know and love is deceptively similar to a property look up on a Javascript object.
- Object property:
myObject["myKey"].runMyFunction();
- jQuery selector:
jQuery(".myClass").slideDown();
Don’t be fooled, every call to $(".myClass")
will re-run your search of the DOM and come back with a new collection; these are not O(1) look-ups! Fortunately there are two ways to avoid making redundant queries, chaining and caching.
var mySuperSlowSelector = $(".myClass:contains('foobar') .myOtherClass:visible");
I can now use mySuperSlowSelector
as many times as I want, I’ve persisted the collection returned from my query so jQuery won’t be re-querying the DOM. In practice any jQuery-heavy page should be caching selectors. If multiple functions are using the same selector don’t be afraid to maintain a centralized cache in a scope accessible to all of them. In the example below we can access selectors through our “SelectorCache” to ensure we never query the DOM more than once.
$(function() { function SelectorCache() { var selectors = {}; this.get = function(selector) { if(selectors[selector] === undefined) { selectors[selector] = $(selector); } return selectors[selector];
} } var selectorCache = new SelectorCache(); function foo() { selectorCache.get("#myId .myClass p").fadeOut(); } function bar() { selectorCache.get("#myId .myClass p").slideDown(); } });
You can’t cache everything
Caching your selectors is like caching your search results. If new nodes are added to your DOM, or attributes within it change, your cached collection of elements won’t “automagically” update. In these situations you’ll need to re-run the query and cache a new set of elements, or you may decide caching isn’t useful if things are too dynamic.
Part 2: The DOM is not a database…
But jQuery sure lets you treat it like it is one. Interactions with the DOM are the slowest operations you can perform in client-side Javascript, which makes it a terrible candidate for maintaining state in your application. It’s better to think of the DOM more like a write-only object and less like something you can query for state information. That said, the convenience of something like tagging a sorted column with an “asc
” or “desc
” class is a prime example of a time when keeping a little state in the DOM can be an acceptable design choice. In the end it’s all about striking the right balance.
data-
attributes were a useful addition but they only increased the temptation of using the DOM to store state. data-
attributes can be useful but remember that jQuery provides the excellent data()
method which can serve the same purpose. data()
allows you to attach arbitrary data to a DOM element in Javascript, without having to actually talk to the DOM. data-
attributes are a nice option when you need to tag elements with extra information while rendering a template, but if you have a choice go with pure Javascript.append()
.detach()
method which removes a node from the DOM and returns it for manipulation. If you’re doing heavy interaction with a DOM node you should detach it while you perform your manipulation and re-insert it when you are finished.
Part 3: A note on events
Events are often another pain point in jQuery-heavy pages and I want to make two quick points.
.click()
when you could just as easily run a function, otherwise you incur the overhead of a DOM event when you trigger an element’s event handler. If you find yourself needing to trigger an element’s event handler, consider moving the contents of the handler into its own stand-alone function. With this pattern the function can be called by the click()
event handler as well as your code when you need to trigger the behavior yourself.<tr>
tags is bound to the parent <table>
, the new rows added on the fly will still trigger the delegate event handler.
The tip of the iceberg
Efficient Javascript and jQuery use is a big subject and we’ve just hit the tip of the iceberg. For an excellent in-depth breakdown you can do no better than Rebecca Murphey’s jQuery Fundamentals. She covers the essentials and outlines best practices in a concise and clear manner. The book should make it onto the required reading list of any developer’s jQuery self-education.