Optimising jQuery selectors in IE7
I'm currently doing a stint at LBi working on a large project which is nearing the initial release. All the functionality has been written and most of the bugs have been squashed, so we're turning our eye to performance. There's a lot of javascript which runs nice and speedily in modern browsers but we're looking at over a second of javascript execution on DOM ready in Internet Explorer 7.
Using dynaTrace to identify the worst offenders I was surprised to see how much time was being spent in selectors, a lot of which weren't finding anything. Over 30 selector calls were being made on every page load, and whether they returned anything or not it was taking IE a long time to process them all.
Simple optimisations
My first attempt at fixing the problem was to simply write better selectors. Since version 1.3 jQuery has been using Sizzle which is a right-to-left or bottom-up parser. Sizzle starts with the right most part of your selector, finds all matches, and then walks up the ancestor tree trying to match the rest of the selector.
A few basics for optimising right-to-left selectors:
- Matching an id is best of all.
- Make the right-most term specific. Include a tagname so Sizzle can use getElementsByTagName instead of checking every element. Thin down the selection by class or attribute.
- Make the left-hand terms vague. Multiple conditions mean each has to be checked for before Sizzle can succeed and stop looking.
- Only include a single ancestor. As soon as a match is found Sizzle will stop looking, so more ancestors just mean more work.
To clarify that rambling here's a couple of examples:
$( "div.foo .bar .baz" ); // bad
$( ".foo a.baz" ); // good
An interesting side note on id matching is that jQuery 1.3.2 doesn't
optimise queries like $( "#id a" ). It finds all anchors and then
searches their ancestors for #id which is not how it behaves if you
use $( "#id" ).find( "a" ). This finds #id first and then grabs all
anchors within it. The second is much faster in the case where #id
doesn't exist, which is perfect for code being run on every page load.
Sizzle already has this optimisation built in so it'll presumably be
sorted in the next jQuery release.
Update: jQuery 1.4 was released a week after I wrote this and I can
confirm that it has this optimisation built in, so $( "#id a" ) takes
the same amount of time in IE as $( "#id" ).find( "a" ) with a 10,000
element DOM.
Compound selectors
The simple fixes helped a lot, but there were still some specific selectors which were running very slowly. Two of the biggest offenders were compound selectors being using for some validation routines:
var inputs = [
'input.areaCode',
'input.subscriberNumber',
'.quickTransferInner input',
'#txtAmountBetweenEnd',
'.sortCode input',
'.amountField input',
'.timeYearsMonths input'
];
$( inputs.join( "," ) ).each( ... );
Instead of making jQuery handle filtering the elements I tried an approach mimicking the right-to-left behaviour of Sizzle but applying the knowledge specific to this situation:
// use word boundaries to make sure we get whole classes
var classes = /\b(areaCode|subscriberNumber)\b/,
parents = /\b(quickTransferInner|sortCode|amountField|timeYearsMonths)\b/;
$( "input" )
.filter( function() {
if ( classes.test( this.className ) ) return true;
var p = this;
while ( p = p.parentNode )
if ( parents.test( p.className ) ) return true;
return false;
} )
.add( "#txtAmountBetweenEnd" )
.each( ... );
The final result was a speed up of around 35ms in IE7 across the two selectors like this. WebKit actually suffered a small slow down but remained fast enough (under 1ms) that it wasn't a concern.