Improvements to jQuery

As advised in Yahoo’s best practices, an extensive use of event delegation has been made in ToDoSo while access to the DOM have been reduced as much as possible.

However, upon careful inspection of jQuery’s internals, it appears that those two rules are contradictory with this library. Indeed, in the case of a document composed of nested <div>s of different colours such as illustrated in the following figures:

Structure of the document

actual document

If event delegation is to be used at the div.grey level to change a class of a div.red to .yellow, the following code would be used:

[code lang="js"]
$("div.grey").find("div.red").live("click", function( event ) {
// event.currentTarget is the clicked red.div
$(event.currentTarget).removeClass("red").addClass("yellow");
});
[/code]

In the current implementation of jQuery, this is what happens when div.white is clicked, before the class can be changed:

  1. the event is captured at the div.grey level, but the event target is the clicked div.white
    1. the DOM is accessed to retrieve all div.red inside the div.grey
    2. a function loops through all the retrieved div.red to check if the event target is one of them, and fails.
  2. the ancestor of the event target (the top left div.black) needs to be checked in the same way
    1. the DOM is accessed to retrieve all div.red
    2. a function loops through them, searching for the parent div.black, without success
  3. finally, the ancestor of the div.black (the top left div.red)  is checked
    1. once again the DOM is accessed to retrieve all div.red
    2. a function loops through them and the top left div.red is one of them
  4. as of jQuery 1.3.3, the currentTarget property of the event object is set to the found div.red

The complexity of this algorithm is O(n * m) or 0(n²):

  • n being the number of level of elements between the initial event target and the element to which the listener is bound,
  • m being the number of elements corresponding to the delegation selector, “div.red” in this case.

However, it appears that, in the case of a delegation selector of the form “div”, “.red” or “div.red”, it is unnecessary to access the DOM and loop through the elements corresponding selector. Instead, it is possible to:

  1. check if the event target is a div and has a class “red”: its class is white
  2. check if the ancestor is a div and has a class “red”: its class is black
  3. check if the ancestor’s ancestor is a div and has a class “red”: the currentTarget has been found.

Although checking the class and type of an element requires an access to the DOM, it does not require to retrieve elements from the document, which is far more expensive, and effectively reduces the complexity of the algorithm to 0(n).

A new implementation of the jQuery.filter() function (used by .live()) yielded the following results for the previous document:

Action Original implementation New implementation
function calls exec. time function calls exec. time
Click in div.grey 75 2.2ms 33 0.8ms
Click in a div.red 46 1.4ms 24 0.6ms
Click in a div.black 78 2.3ms 34 0.8ms
Click in a div.white 94 2.7ms 39 0.9ms

Those results make the reduced complexity obvious and show a decent performance improvement. The test has been run on a simple document with a high end machine and the latest version of Firefox (3.5). The performance gain is expected to be significant when run on less powerful hardware with older browser.

Moreover, click events occur rather rarely on the document, but mouseover, mouseout and mousemove events occur much more frequently as the user moves the cursor within a page. Dealing with those event is far more efficient with the new implementation, as long as the delegation selector conforms the previously specified pattern. This limited choice of “efficient selectors” proved to be sufficient when developing ToDoSo.

An advantage of improving the jQuery.filter() function rather than the .live() function directly is that it benefits all function of jQuery using the former: .filter(), .is(), .hasClass(), .closest() and .live().