8 jQuery Micro Optimization Tips

April 26, 2010

Warning: Please do not go back to old code and start rewriting based on the following optimizations. While they provide a performance boost, it is minimal at best, and not worth all the extra work of rewriting your applications. These tips are meant for improving future work only.

Warning 2: Most of these tips break the 'Write Less, Do More' jQuery motto, so use with caution and balance elegance with performance correctly.


Background

To start, let's review the jQuery function and it's init constructor.

// Define a local copy of jQuery
var jQuery = function( selector, context ) {
		// The jQuery object is actually just the init constructor 'enhanced'
		return new jQuery.fn.init( selector, context );
	},

Above is taken directly from the jQuery-1.4.2 source file. So every time you call the jQuery function, a new jQuery instance is created. It's important to keep this in mind, to limit the number of selections you do make.

init: function( selector, context ) {
	var match, elem, ret, doc;

	// Handle $(""), $(null), or $(undefined)
	if ( !selector ) {
		return this;
	}

	// Handle $(DOMElement)
	if ( selector.nodeType ) {
		this.context = this[0] = selector;
		this.length = 1;
		return this;
	}
	
	// The body element only exists once, optimize finding it
	if ( selector === "body" && !context ) {
		this.context = document;
		this[0] = document.body;
		this.selector = "body";
		this.length = 1;
		return this;
	}

	// Handle HTML strings
	if ( typeof selector === "string" ) {
		// Are we dealing with HTML string or an ID?
		match = quickExpr.exec( selector );

		// Verify a match, and that no context was specified for #id
		if ( match && (match[1] || !context) ) {

			// HANDLE: $(html) -> $(array)
			if ( match[1] ) {
				doc = (context ? context.ownerDocument || context : document);

				// If a single string is passed in and it's a single tag
				// just do a createElement and skip the rest
				ret = rsingleTag.exec( selector );

				if ( ret ) {
					if ( jQuery.isPlainObject( context ) ) {
						selector = [ document.createElement( ret[1] ) ];
						jQuery.fn.attr.call( selector, context, true );

					} else {
						selector = [ doc.createElement( ret[1] ) ];
					}

				} else {
					ret = buildFragment( [ match[1] ], [ doc ] );
					selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes;
				}
				
				return jQuery.merge( this, selector );
				
			// HANDLE: $("#id")
			} else {
				elem = document.getElementById( match[2] );

				if ( elem ) {
					// Handle the case where IE and Opera return items
					// by name instead of ID
					if ( elem.id !== match[2] ) {
						return rootjQuery.find( selector );
					}

					// Otherwise, we inject the element directly into the jQuery object
					this.length = 1;
					this[0] = elem;
				}

				this.context = document;
				this.selector = selector;
				return this;
			}

		// HANDLE: $("TAG")
		} else if ( !context && /^\w+$/.test( selector ) ) {
			this.selector = selector;
			this.context = document;
			selector = document.getElementsByTagName( selector );
			return jQuery.merge( this, selector );

		// HANDLE: $(expr, $(...))
		} else if ( !context || context.jquery ) {
			return (context || rootjQuery).find( selector );

		// HANDLE: $(expr, context)
		// (which is just equivalent to: $(context).find(expr)
		} else {
			return jQuery( context ).find( selector );
		}

	// HANDLE: $(function)
	// Shortcut for document ready
	} else if ( jQuery.isFunction( selector ) ) {
		return rootjQuery.ready( selector );
	}

	if (selector.selector !== undefined) {
		this.selector = selector.selector;
		this.context = selector.context;
	}

	return jQuery.makeArray( selector, this );
},

There is a bit of magic inside the init constructor, most of which is outside the scope of this article. Here's a brief rundown of what's happening

  1. If an empty string, null, or undefined is passed in as the selector, an empty jQuery object is returned.
  2. If a DOM Element is passed, then it is stored inside the jQuery object wrapper and returned.
  3. If 'body' is passed, then a quick reference to the body element is wrapped into the jQuery object and returned.
  4. If a string is passed, jQuery/Sizzle traverses the DOM off the document/context element and returns the set wrapped inside a jQuery object
  5. If a function is passed, it gets added to the document ready handler set, and the root document element wrapper is returned
  6. Lastly, jQuery assumes another object is passed in, and injects that object into the jQuery wrapper

1. jQuery.root

Internally, all selectors that don't provide a context use jQuery( document ).find( selector ). Save yourself some ms, store the document root onto jQuery itself, and then run all global selectors off of that element. Take a look:

// Super Slow (not really, but still slower than it needs to be)
jQuery('ul.special-selector').doSomething();

// Faster (assuming you do more than 1-2 global selections)
jQuery.root = jQuery( document );
jQuery.root.find('div.special-selector').doSomething()

2. Context sucks, use find

Don't get me wrong, you should always run selections based on a context if possible. But passing in a context to the jQuery constructor creates an extra unneeded function call. Internally jQuery runs context.find( selector ) anyway, so skip that step:

// Extra Function Call
jQuery( 'div', context ).doSomething();

// Same functionality, minus the extra instance and function call
context.find('div').doSomething();

3. Live is terrible, delegate is awesome

The best part: delegate is live with a context. So why is live a bad idea? To use live, you first have to run a selection on the page, and then bind the live handler. Something like:

// Example of live binding
jQuery('div').live('click', fn);

This creates a new jQuery instance and adds all div elements on the page to the collection. Then, after all the traversing and processing, those elements get ignored. Live simply takes the selector string ('div'), binds a click element to the document, and tracks all click events on the page that match the 'div' selector.

So if you load a page with 400+ div elements, the above code example will find all 400 elements, and then ignore them. So how do we make this better?

// A Faster Live Event
jQuery.root.delegate('div', 'click', fn);

Using the cached document element from the first example on this page, we bind the same live function (through delegate) to the document. It's the same functionality as live, without the DOM traversal.

4. jQuery.data > jQuery.fn.data

If you are getting/setting single key values, always use jQuery.data over the jQuery.fn.data method. Here's a common example and the fix for it.

// Wasted instance and two extra function calls
elem.click(function(){
	if ( jQuery(this).data('flag') == true ) {
		// do something...
	}
});

// Faster data process
elem.click(function(){
	if ( jQuery.data( this, 'flag' ) == true ) {
		// do something...
	}
});

The second example works the same as the first, minus two function calls and a new jQuery object. Here's the speed culprit:

jQuery(this)

This part not only adds a useless function call, it creates a new instance of the jQuery object, both of which can be avoided by simply going directly to the base data method. To add to this, the data plugin uses the base data method internally. Here's a simplified example of how the data plugin works:

jQuery.fn.data = function( key, value ) {
	if ( value === undefined ) {
		return jQuery.data( this[0], key );
	}
	else {
		return this.each(function(){
			jQuery.data( this, key, value );
		});
	}
};

As you can see, in this very simplified version, jQuery uses the base data method internally. Skip the extra function calls, and go directly to the source. Here's one last example showing the setter method.

// Wasted  time
elem.click(function(){
	jQuery(this).data( 'flag', true );
});

// Faster
elem.click(function(){
	jQuery.data( this, 'flag', true );
});

I should note, that skipping the jQuery.fn.data step will not trigger the getData / setData events. If you happen to use these, then you will need to keep using the data plugin, as it's the only one that triggers those events

5. Bind and Trigger, get used to it

jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
	"change select submit keydown keypress keyup error").split(" "), function( i, name ) {

	// Handle event binding
	jQuery.fn[ name ] = function( fn ) {
		return fn ? this.bind( name, fn ) : this.trigger( name );
	};

	if ( jQuery.attrFn ) {
		jQuery.attrFn[ name ] = true;
	}
});

Nothing better than going directly to the source. All the event methods, like click, focus, blur, submit, etc., are just short-hand methods mapping to bind and trigger, only with an extra function call. Getting used to using bind and trigger directly will not only reduce the number of function calls, but will also help you when working with larger applications that require namespacing your events.

6. Each is evil

each: function( object, callback, args ) {
	var name, i = 0,
		length = object.length,
		isObj = length === undefined || jQuery.isFunction(object);

	if ( args ) {
		if ( isObj ) {
			for ( name in object ) {
				if ( callback.apply( object[ name ], args ) === false ) {
					break;
				}
			}
		} else {
			for ( ; i < length; ) {
				if ( callback.apply( object[ i++ ], args ) === false ) {
					break;
				}
			}
		}

	// A special, fast, case for the most common use of each
	} else {
		if ( isObj ) {
			for ( name in object ) {
				if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
					break;
				}
			}
		} else {
			for ( var value = object[0];
				i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
		}
	}

	return object;
},

Actually, each is a pretty awesome utility function. The problem is that there is only one true reason to use each, and that is when a closure is needed for each item. If you are just looping through an array, then the callback function gets triggered on every iteration. So using an array of 25 items, the callback gets triggered 25 times. That can really add up depending on the size of your array.

7. Classes over styles

Anytime you are adding/changing more than one style on an element(s), you should be using a class. Passing an object of changes to the css method runs a loop over the object, with a minimum of 3 function calls on each iteration through jQuery internals. This can all be skipped by using addCass / removeClass that holds the required styles. I would go so far as to say that using a class for a single style change is better than running through jQuery's style module.

// Slow and maintenance unfriendly
elem.css({ color: 'red', font-weight: 'bold' });

// Simple and elegant
elem.addClass('error');

8. Object.length, use it.

Every jQuery selection comes with a length property that defines how many elements were found. Always check to make sure that there is a set of elements in your object before running a chain of methods. Take the following:

jQuery('NoElementsSelector').trigger('click').addClass('blue');

In this sample, even though the selector is obviously not going to return any elements, both the trigger and addClass methods will still be called. On top of that, the trigger method calls the jQuery.fn.each function internally, which in turn calls the jQuery.each function to iterate over an empty list. That's a total of 4 function calls that should have never been run in the first place.

var $elem = jQuery('NoElementsSelector');
if ( $elem.length ) {
	$elem.trigger('click').addClass('blue');
}

With a simple if check, we have removed a total of 4 useless function calls. Don't be lazy, if check your objects.

jQuery is a crutch, not the solution

Never forget about plain old javascript, which is all that jQuery is. Always explore native options before falling back onto jQuery, not the other way around. Taking the above css example, reworked as native javascript.

// Slow and maintenance unfriendly
elem.css({ color: 'red', font-weight: 'bold' });


// Still maintenance unfriendly, but faster and more direct
for ( var i = 0; i < elem.length; i++ ) {
	elem[i].style.color = 'red';
	elem[i].style.fontWeight = 'bold';
}

Now I am in no way advocating that you lose the abstraction that jQuery provides, but rather, providing a faster alternative that doesn't require the use of jQuery. Always keep in mind that while not as elegant, native javascript can do what you want faster.

Other Resources

jQuery Source - Git it, watch it, learn it.
Firequery - Firebug extension for jQuery development
Paul Irish - Great slide show on jQuery performance
Selector Performance - Unit testing results of jQuery selectors
Nettus - Range of performance increases
Have a question or comment? ask@codenothing.com.