Skip to content

jQuery - The Little Things

»It works!« is a state any piece of code needs to achieve. It's the goal most developers have in order to get the job done. But sadly »It works!« is more often than not, simply insufficient. It's a state that's only true for the moment. This post explains a few problems found in your average jQuery-based code.

A couple of days ago the following snippet made its way into my Twitter timeline:

$("#menu > ul > li").on("mouseenter", function() {
  $(">ul", this).stop().fadeIn(300);
}).on("mouseleave", function() {
  $(">ul", this).stop().fadeOut(300);
});

as you can see in the fiddle, »It Works!«:

while this code does what is expected (namely showing/hiding nested lists), it has a couple of problems that are not visible and thus not apparent to the developer.

I won't be explaining anything that hasn't been covered in other blogs 1232343234 times in the last couple of years. But apparently we can't stop repeating these things over and over again.

Registering Events

Registering multiple event handlers on the same set of elements:

$(selector)
  .on("mouseenter", function() { /* … */ })
  .on("mouseleave", function() { /* … */ })

can be replaced by a single call to .on():

$(selector).on({
  mouseenter: function() { /* … */ }),
  mouseleave: function() { /* … */ })
});

Instead of manually registering mouseenter and mouseleave events, we could have simply registered a hover:

$(selector).hover(
  function() { /* mouseenter */ },
  function() { /* mouseleave */ }
);

And just to cover this too, you can register the same handler for multiple events:

$(selector).on("mouseenter mouseleave", function() { /* … */ });

All of this (and quite some more) is explained in the docs.

Event properties

When jQuery invokes event handlers, it passes an $.Event instance. This object contains all data associated with the event. It's a normalization of the native event objects (IE vs. real browsers) which you can still access through event.originalEvent.

We've previously established that we can register a single function for multiple events. But now we have to somehow identify if we are to show or hide the nested menu. We could do that by checking the list's display status:

$("#menu > ul > li").on("mouseenter mouseleave", function(event) {
  if ($(">ul", this).css("display") == "none") {
    $(">ul", this).stop().fadeIn(300);
  } else {
    $(">ul", this).stop().fadeOut(300);
  }
});

Or we could ditch the slow and ugly DOM access and simply ask event.type:

$("#menu > ul > li").on("mouseenter mouseleave", function(event) {
  if (event.type === "mouseenter") {
    $(">ul", this).stop().fadeIn(300);
  } else {
    $(">ul", this).stop().fadeOut(300);
  }
});

Delegated Events

I couldn't explain the basics of event delegation any better than Steve Schwartz. If you don't know how delegating events works, read that post!

Thanks to event delegation, we stop binding the mouseenter and mouseleave events to every single <li> in our menu. Instead, we bind one event handler to the menu, and make sure it's only triggered for events happening on our "top-level" <li>:

$("#menu").on("mouseenter mouseleave", "#menu > ul > li", function(event) {
  if (event.type === "mouseenter") {
    $(">ul", this).stop().fadeIn(300);
  } else {
    $(">ul", this).stop().fadeOut(300);
  }
});

Benefits of delegating events

  • Quicker page load: DOM is only burdoned to register one event handler on the <ul>, rather than registering a handler for each <li>
  • less memory consumption: 1 event handler instead of many
  • New <li> automatically "inherit" the event handling

Considering the Context

Event delegation is possible because events "bubble" through the DOM. So each parent element in the hierarchy (up to document) is asked if it has a handler bound for event. If so, the handler is executed and the event bubbles up to the next parent element.

$(document).on("mouseenter", "#menu > ul > li", function() { console.log("document"); });
$("#menu").on("mouseenter", "#menu > ul > li", function() { console.log("#menu"); });

The above code prints #menu document. That is because document is the very last element in the bubbling chain. Knowing this, and the fact that we can abort the bubble process with event.stopPropagation() or event.stopImmediatePropagation() we can make sure that the event bubble stops at #menu:

$(document).on("mouseenter", "#menu > ul > li", function() { console.log("document"); });
$("#menu").on("mouseenter", "#menu > ul > li", function(event) {
  event.stopImmediatePropagation();
  console.log("#menu");
});

The above code prints #menu. The handler on document is never executed, because the bubble was stopped.

With that in mind, our handler should now look like this:

$("#menu").on("mouseenter mouseleave", "#menu > ul > li", function(event) {
  event.stopImmediatePropagation();
  if (event.type === "mouseenter") {
    $(">ul", this).stop().fadeIn(300);
  } else {
    $(">ul", this).stop().fadeOut(300);
  }
});

Selectors

There is tons of material on the web explaining jQuery selector performance. I'm not going to repeat all of that, just what's important to us in this case:

  • use $(context).find(selector) instead of $(selector, context)
  • use $(context).children("ul") instead of $(context).find(">ul")

Most of jQuery's traversal functions accept a selector just like .find(). In the case of .children() (and .siblings(), .next() and so on) the selector is used as a filter. So $(context).children() will give you all direct descendants, and $(context).children("ul") will filter that down to all direct descendant <ul>.

With that in mind, our handler should now look like this:

$("#menu").on("mouseenter mouseleave", "#menu > ul > li", function(event) {
  var $ul = $(this).children("ul").stop();
  event.stopImmediatePropagation();
  if (event.type === "mouseenter") {
    $ul.fadeIn(300);
  } else {
    $ul.fadeOut(300);
  }
});

Just take a minute and browse through jQuery's traversing functions. I bet there are a couple you haven't heard of yet.

Accessing Properties

There are two ways to access an object's properties:

var x = "bar";
var foo = {
    bar: "hello world";
};

foo.bar === "hello world";
foo["bar"] === "hello world";
foo[x] === "hello world";

With that in mind, our handler should now look like this:

$("#menu").on("mouseenter mouseleave", "#menu > ul > li", function(event) {
  var $ul = $(this).children("ul").stop(),
    fade;

  if (event.type === "mouseenter") {
    fade = "fadeIn";
  } else {
    fade = "fadeOut";
  }
 
  event.stopImmediatePropagation();
  $ul[fade](300);
});

Ternary Operator

Do you know the ternary operation?

$("#menu").on("mouseenter mouseleave", "#menu > ul > li", function(event) {
  var $ul = $(this).children("ul").stop(),
    fade = event.type === "mouseenter" ? "fadeIn" : "fadeOut";

  event.stopImmediatePropagation();
  $ul[fade](300);
});

Shrinking Things

Since both variables $ul and fade are only used once, they could also be replaced by inline code:

$("#menu").on("mouseenter mouseleave", "#menu > ul > li", function(event) {
  event.stopImmediatePropagation();
  $(this).children("ul").stop()[event.type === "mouseenter" ? "fadeIn" : "fadeOut"](300);
});

Strictly speaking, this should be the most performant scenario. We're not (only) writing code for the computer, but for other developers as well. While dropping the variable declarations may be nice from a performance point of view, the resulting code gets more complex to read.

I do not recommend taking this step. Keep code readable, even if that makes it a little more verbose.

Hard-Coding Options

Baking possibly configurable values into your source code is a bad idea - simply because you have no way of changing these properties later on. Try to avoid writing code that does things like .fadeIn(300). Those 300 milliseconds may look awesome on your development machine, but totally crap out on some old computer running IE6 (or on an iPhone, iPad, iWhatever). This doesn't solely apply to animations, but given the example code, we'll focus on that.

If you've ever read the .animate() docs, you'll probably know about "slow" and "fast". In its animation functions, jQuery accepts numbers (being a duration in milliseconds) and strings (being names of predefined durations). These predefined durations are accessible in jQuery.fx.speeds:

jQuery.fx.speeds = {
    slow: 600,
    fast: 200,
    // Default speed
    _default: 400
};

$(selector).fadeIn("foobar") will sillently fall back to jQuery.fx.speeds._default (because "foobar" wasn't defined).

For our script we could've setup jQuery.fx.speeds.listlist = 300; and used it by replacing $ul[fade](300); with $ul[fade]("listlist");. With that, we have the option of changing the 300 milliseconds whenever we want or need to.

Conclusion

Continue reading / watching jQuery Anti-Patterns for Performance by Paul Irish.

Comments

Display comments as Linear | Threaded

Marc on :

MarcGreat article! It is a very good approach to teach like that: Take something which works and turn it to something that works and is performant. Would love to see more of these examples.

Cheers, Marc

Rodney Rehm on :

Rodney RehmHm… I forgot to mention to use .slideDown() instead of .fadeIn(). The latter will acquire space instantly and then fade in. That makes the following content jump. slideDown will gently move the following content to the right position.

Marc on :

MarcWell, this example is for a navigation bar with a second level sliding out (above the content), so I think fadeIn is correct with an absolute positioned ul..

Maicon Sobczak on :

Maicon SobczakIs always important revise our way of coding to improve our skills and quality of the websites. Well explained article that made me think about some aspects of my code.

The author does not allow comments to this entry