There were a lot of good speakers at today’s jQuery Summit, but Paul Irish‘s talk on jQuery Anti-Patterns for Performance & Compression was my stand-out favorite. Covering a number of advanced jQuery performance optimization techniques, this speech put my knowledge of jQuery optimization to shame.
Before Paul’s talk my understanding of jQuery performance tuning was fairly simplistic:
- Optimize selectors to descend from an id if possible.
- Use tag names when selecting classes and don’t use an excessive number of selectors.
- Define variables instead of selecting the same object repeatedly.
- Optimize your code replacing repetition with object oriented functions.
But the information Paul presented blew all that out of the water. Here are 10 jQuery performance rules I gathered from his presentation:
1. Optimize selectors for Sizzle’s ‘right to left’ model
As of version 1.3, jQuery has been using the Sizzle Javascript Selector Library which works a bit differently from the selector engine used in the past. Namely it uses a ‘right to left’ model rather than a ‘left to right’, which means that given a selector string such as $('div.class span.class2')
the engine starts the search on the right using the most specific selector (the span.class2
) and then checks each ancestor against div.class
rather than starting on the left and checking descendants.
But what does that mean for you? Make sure that your right-most selector is really specific and any selectors on the left are more broad. For instance use:
$('.class span.class2')
rather than:
$('div.class .class2')
And of course descending from an #id is still always best:
$('#id div.class')
2. Use live() not click()
jQuery’s new event handler live()
is a much faster alternative to click()
. It comes with an additional benefit that I am extremely excited about: the selector works even with dynamic content, meaning no more reattaching click events every time you change a piece of content.
Live is faster and better for all click events and most other jQuery event handling, however stay away from it for tracking onmouseover or onsubmit.
3. Pull elements off of the DOM while you play with them
Let’s say you have a form that you want to append a series of inputs to. Normally you might do this:
var theForm = $('#myForm');
inputArr.each(function() {
theForm.append(this);
});
However it’s better to pull the form element off of the DOM while you play around with it:
var theForm = $('#myForm');
var formParent = theForm.parent();
theForm.remove();
inputArr.each(function() {
theForm.append(this);
});
formParent.append(theForm);
4. Use find() rather than context
With jQuery selectors, context is good for performance, but find()
is better. So instead of using context like this:
var panels = $('div.panel', $('#content'));
It is better to use find()
:
var panels = $('#content').find('div.panel');
5. Use HTML 5
HTML 5 means more tags: <section>
, <header>
, <footer>
, etc. More tags mean that there are less items that share a given tag name, which translates into better selector performance.
6. Append style tags when styling 15 or more elements
When styling a few elements it is best to simply use jQuery's css()
method, however when styling 15 or more elements it is more efficient to append a style tag to the DOM:
$('<style type="text/css"> div.class { color: red; } </style>')
.appendTo('head');
7. Test selectors using Google Page Speed
The CSS selector optimization rules used by FireBug's Page Speed addon also apply to jQuery. If you want to test how well your selector is optimized, apply it with CSS and see if any selectors are flagged by Page Speed.
8. Use object detection even if jQuery doesn't throw an error
It's great that jQuery methods don't throw a ton of errors at your users, but that doesn't mean that as a developer you should just rely on that. Even though it won't throw an error, jQuery will have to execute a number of useless functions before determining that an object doesn't exist. So use a quick object detection before calling any methods on a jQuery object that may or may not exist.
9. Use direct functions rather than their convenience counterparts
If you look at the jQuery core you'll notice a variety of functions are just convenience methods for calling other functions, for instance if you call getJSON()
it calls get()
which calls ajax()
. You can cut this extra work out of the loop by referencing the direct function ajax()
.
10. Learn the lesser known methods
With a vast and evolving library like jQuery it can be hard to keep on top of all the methods but it is still very important. Some methods Paul pointed out specifically are map()
, slice()
, stop()
, queue()
, dequeue()
, prevAll()
, pushStack()
and inArray()
.
Learning the variety of methods in your toolkit will help you to better use this library and write more concise code. The worst thing you can do is write a custom function for something that already exists in the jQuery core: besides bloating your Javascript, it would take a lot of effort to write code that is optimized as well as it is by the elite and enormous group of developers who work on and debug the jQuery core.
For more info about Paul Irish, check out his blog, YayQuery podcasts or follow him on Twitter.
Awesome write-up Jon! I love Paul’s performance talk.
Caught something in #9:
“for instance ajax() uses getJSON()”
Technically I think it’s the other way around. From the jquery source:
getJSON: function( url, data, callback ) {
return jQuery.get(url, data, callback, "json");
}
get() is a convenience method for ajax:
get: function( url, data, callback, type ) {
// shift arguments if data argument was ommited
if ( jQuery.isFunction( data ) ) {
callback = data;
data = null;
}
return jQuery.ajax({
type: "GET",
url: url,
data: data,
success: callback,
dataType: type
});
}
Other than that, great summary!
Nice catch Alex, thanks for pointing that out and sorry I didn’t do my homework on that one 🙂
Hi,
Nice article, just a little catch on the context syntax,
Recommend you look at Brandon Aaron’s latest blog entry:
http://brandonaaron.net/
To quote:
”
$(‘a’, ‘#myContainer’).context; // => document
When jQuery encounters another selector for the context it actually converts it to say the following instead.
$(‘#myContainer’).find(‘a’);
”
To set the context you would either get the html element for #myContainer by indexing the jquery wrapped set and passing that in, or more preferably IMO do:
$(‘a’, document.getElementById(‘myContainer’));
Nice article though 🙂
Nice catch Joseph – I had meant that to use a jQuery selector for that ID. (And yes I know how stupid it would be to use a selector for the context, I just want a selector here to make the example clearer).
But that’s news to me on being able to pass document.getElementById(‘blah’) to the jQuery context, seems like that could be useful. Do you have any idea whether it is better performance-wise to do that rather than
$('blah').find('blah')
?yes, definitely should do it to get the performance benefit of context. Passing the jQuery object still requires a search of the entire document for the reference (as it doesn’t set a context)
Another way of coding it (from Brandon’s site)
”
// get the node for the context
var context = $(‘#myContainer’)[0];
// pass the context as the second argument
$(‘a’, context).context; // =>
“
@joseph – Well I know you shouldn’t pass a selector for the context, but what I’m wondering is whether it is more efficient to pass the getElementById() or a predefined jQuery object. (For anyone else reading here, in Joseph’s example the
$('#myContainer')[0]
is functionally equivalent todocument.getElementById('myContainer')
)So let’s say you have:
I think what you’re saying is that it’s more efficient to do:
rather than:
Is that right? (And thanks again for chiming in here – I’m a dork for JS performance :))
I remember in the dark recesses of my mind that document.getElementById(‘myId’) is meant to be slightly faster than $(‘#myId’), I haven’t got metrics, (but assuming native js is always faster has stood me in good stead ;)), I think even in this very simple case jQuery isn’t purely just a wrapper on document.getElementById.
As for what you are saying, yes, $obj[0] is the way to pass in the context, as you shouldn’t really pass in a jQuery object* and by indexing, you are getting the html node which is what will really speed things up 🙂
* now if you pass in a jQuery object it can seem faster, but that is because you are doing a more specific selector on the sizzle engine rather than using a context.
Remember:
$('a', '#myContainer').context; // => document
becomes:
$('#myContainer').find('a');
It would be interesting to see some metrics on all this, but I’m up to my eyeballs in coding at the mo! 🙂
Yeah it makes sense that the native JS method would be faster, thanks for taking time out from being swamped to discuss this one with me 🙂 If I see anything on metrics regarding this, I’ll be sure to post it here.
Great tips. Will start digging more into html 5.
Thank you for sharing. Very useful tips.
Frank
I realize that this post is a little old, but stuff lives forever on the web (until it’s deleted). The advice to use “.live()” instead of direct handlers on elements is good, but now in 2011 I think it’s even better to encourage the use of “.delegate()” over the use of “.live()”.
if you call getJSON() it calls get() which calls ajax(). You can cut this extra work out of the loop by referencing the direct function ajax()
That’s nuts! We’re talking about take 3 or 4 function calls out of the call stack, that won’t even give you enough performance worth mentioning.
Calling getJSON() makes code easier to read and follow. I’m willing to trade of nanoseconds for that.
That seems great but for points 2, 3 and 4, do you have performance test ?
I don’t mean that you are wrong, but i’d like to decide if it’s worth to rewrite my code and get one and only method.
Best regards from Paris, France.
S.
I aggre with Mike “Mike McNally says:
I realize that this post is a little old, but stuff lives forever on the web (until it’s deleted). The advice to use “.live()” instead of direct handlers on elements is good, but now in 2011 I think it’s even better to encourage the use of “.delegate()” over the use of “.live()”.
Thanks fo sharing
Here is some information about the .live() in jQuery 1.7
“As of jQuery 1.7, the .live() method is deprecated. Use .on() to attach event handlers. Users of older versions of jQuery should use .delegate() in preference to .live().”
http://api.jquery.com/on/
Jon,
I see that this is a pretty old post, still want to ask something…
For the point 3, can I make a copy of DOM element using clone()? Will it produce a copy seperate from DOM?
Regarding .live and .delegate, since 1.7, “.live” and “.delegate” have been rolled into “.on”: http://api.jquery.com/on/
It’s 2012 now and time has come to drop the use of “.delegate()” in favour of “.on()”.
with jquery 1.7, .on() was created which outperforms live(). use .on()
In #8, you say “Use object detection even if jQuery doesn’t throw an error”
I just want to understand. Let’s say I’m using a site-wide script file where I do jQuery using selectors that target content on virtually page on my site. Now let’s say, I have a simplified page that doesn’t have all the the same target conent items. If I’m reading you correctly, I should test first to see if something exists before performing actions on that code?
For example: I have a div that I use AJAX to fill with data from a blog’s RSS feed. On one page, I need the room for a larger table of other data, so I drop the box showing the RSS feed. Now the code to fill that div is in my site-wide included script file. I should probably test to see if the div is present before calling the AJAX code, right?
I think I answered my own question…
The points you have mentioned are definitely great,
i need to update your point2,
As per jquery documentation
Use of the .live() method is no longer recommended since later versions of jQuery offer better methods that do not have its drawbacks. In particular, the following issues arise with the use of .live():
jQuery attempts to retrieve the elements specified by the selector before calling the .live() method, which may be time-consuming on large documents.
Chaining methods is not supported. For example, $( “a” ).find( “.offsite, .external” ).live( … ); is not valid and does not work as expected.
Since all .live() events are attached at the document element, events take the longest and slowest possible path before they are handled.
On mobile iOS (iPhone, iPad and iPod Touch) the click event does not bubble to the document body for most elements and cannot be used with .live() without applying one of the following workarounds:
Use natively clickable elements such as a or button, as both of these do bubble to document.
Use .on() or .delegate() attached to an element below the level of document.body, since mobile iOS does bubble within the body.
Apply the CSS style cursor:pointer to the element that needs to bubble clicks (or a parent including document.documentElement). Note however, this will disable copy\paste on the element and cause it to be highlighted when touched.
Calling event.stopPropagation() in the event handler is ineffective in stopping event handlers attached lower in the document; the event has already propagated to document.
The .live() method interacts with other event methods in ways that can be surprising, e.g., $( document ).off( “click” ) removes all click handlers attached by any call to .live()!