Link to wealthfront.com

Fork me on GitHub

Wednesday, October 20, 2010

jQuery the Right Way

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.
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.

Luckily this isn’t hard to do. ID selectors are fast, so fast, in fact, that they can be used to jump to a node deeper in the tree before beginning your search. For this reason jQuery optimizes for selectors that are rooted with an ID selector.

Using $(".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.

If you can’t root in an ID selector you can at least narrow the search with an element selector. While not quite as quick as an ID lookup, it still lets jQuery use 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.

Once you’ve got a collection jQuery provides a whole range of DOM traversal methods that are at your disposal. You can use the traversal methods as well as 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.

When you chain jQuery functions the collection retrieved by the selector gets passed to each successive function. As a result the query never has to re-run since the collection of elements get passed along the chain until it completes. However chaining is only convenient in situations in which you want to perform multiple actions on a collection in a single place in your code. Much better is to cache your selectors in a variable for reuse in whatever manner you see fit:
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.

The introduction of HTML5’s 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.

While we’re on the subject of DOM manipulation it’s worth noting that jQuery has made some serious progress in speeding up DOM writes recently, but you still need to take care in your DOM manipulation. In general treat every DOM insertion as the costly action it is, minimize DOM touches by building up HTML strings and doing a single append().

As of version 1.4 jQuery also provides the 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.

First, avoid triggering events yourself in code with functions like .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.

Second, if you find yourself attaching the same event handler to a large number of elements it’s a good sign you should be using jQuery’s delegates. Delegates allow you to attach an event handler to a common parent of your elements instead of attaching a large number of discrete handler functions to each individual element. In addition to the increase in speed, delegates have the added advantage of firing for new DOM nodes too. If, for example, the delegate for your <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.



Update 10/23: Thanks to Perceptes on Reddit for pointing out a couple errors in the SelectorCache example.