Speed Up Your Moo Part 1: Selectors
I never expected myself to do that. In any discussion that was coming up I was the one saying "It is not important anyway". And I still think it does not matter in most cases. However, speed can be an issue and I'm going to post a series of posts that will help you increase the performance of your applications heavily. I will do that by either releasing small snippets that can speed up MooTools itself or by posting best-practice examples. The first part focusses on Selectors.
Unit A: The Theory
(If you know all that just jump down to unit B)
I often see people asking for help and posting code that has tons of bad practice in it. I'm not going to dive into anything complex here, I'm only covering the ground. In theory you should optimize your selectors as much as possible. There are numerous ways to do that but please bear in mind that you can only measure the difference by looking at your whole application, not just single lines. If you follow my guidelines you will be able to avoid any bottlenecks you may have encountered in previous applications you created.
Always cache your results
This is probably one of the most important aspects. Don't run your selectors more often than needed. Keep your code clean, try to minimize the use of the same selectors as much as possible, try to maximize the use of chaining where you can:
// Bad practice
$$('.myElements').setStyle('marginTop', 10);
$$('.myElements').setStyle('marginLeft', 5);
$$('.myElements').addEvent('click', function(){ ... });
// Good practice
$$('.myElements').setStyles({
marginTop: 10,
marginLeft: 5
}).addEvent('click', function(){ ... });
// Cache the Elements
var elements = $$('.myElements');
elements.setStyle('marginTop', 10);
elements.setStyle('marginLeft', 5);
elements.addEvent('click', function(){ ... });
The bad practice example executes the same selector for three times. This means parsing the selector string and traversing all elements to check for the class "myElements". This could take some time on a big website. Of course, we are talking about a few milliseconds here, but it adds up on bigger projects.
The good practice example uses chaining. It selects the elements once and because every MooTools Element method returns the collection again we can immediately call another function on it.
The third example works if you don't want to chain the methods for readability reasons. Whatever suits you. But still, it only executes the selector once and the same collection is used for every subsequent method call.
With these simple optimizations your script easily runs 3 times faster. In addition, as you can probably understand now, this is why I was always against built-in caching in selector engines. It doesn't work in a clean way across browsers and you should not execute the same selector all the time anyway.
Look in the right spot
When you search for something in your room you don't start at the top and go down to the bottom. You start searching where you think the desired item is located. You start searching for clothes in your closet and your phone is most likely somewhere near a power plug and the same is true on a well-structured HTML website. The links in your sidebar are most likely descendants of a sidebar element, the logo is probably located in the header section. This solely depends on how you structure your website and of course, you know every detail of it. Why not pass on your knowledge to the selector engine?
// Bad practice
$$('.sidebarLinks').doSomething();
// Good practice
$('sidebar').getElements('.sidebarLinks').doSomething();
If you know for certain where your elements reside, use the "getElements" method on another element. It works exactly like $$ with the difference that it does not search the whole document but only within the specified element. Less elements to test the selector against, more speed.
Always specify a tag name
Again, do this only if you know for certain what your structure looks like, but including the tag name when you are looking for a specific class can make a huge difference:
// Usual practice
$$('.myClass').doSomething();
// Better practice
$$('div.myClass').doSomething();
Unit B: Using document.querySelectorAll in MooTools 1.2
We all know by now that MooTools 2.0 will feature a new selector engine called Slick that will be like a billion times faster and Thomas, Valerio and Scott are heavily working on it. But we also know that it will take some more time until MooTools 2.0 will be released.
I'm currently working on a project where I'm not in charge of the HTML structure and I'm only doing the JavaScript code. I tried to increase the performance and I found that one of the best ways is to directly hack into the selector engine and implement querySelectorAll and getElementsByClassName for those browsers that have it. Drop the code below into your scripts and feel the speed difference.
/* SELECTOR OPTIMIZATIONS */
(function(){
var getByClass = (function(){
var testee = document.createElement('div');
testee.innerHTML = '<a name="' + $time() + '" class="€ b"></a>';
testee.appendChild(document.createComment(''));
if (!testee.getElementsByClassName || !testee.getElementsByClassName('b').length) return false;
testee.firstChild.className = 'c';
return (testee.getElementsByClassName('c').length == 1);
})();
Selectors.Utils.search = function(self, expression, local){
var splitters = [];
var selectors = expression.trim().replace(Selectors.RegExps.splitter, function(m0, m1, m2){
splitters.push(m1);
return ':)' + m2;
}).split(':)');
var items, filtered, item;
for (var i = 0, l = selectors.length; i < l; i++){
var selector = selectors[i];
/* CUSTOM */
if (i == 0 && self.querySelectorAll && (/^(?:\w+|\*)?(?:#|\.)?(?:\w+|\*)$/).test(selector)){
try { items = $A(self.querySelectorAll(selector)); } catch (e){}
if (items) continue;
}
if (i == 0 && getByClass && (/^\.(?:\w+|\*)$/).test(selector)){
items = $A(self.getElementsByClassName(selector.substr(1)));
continue;
}
/* END CUSTOM */
if (i == 0 && Selectors.RegExps.quick.test(selector)){
items = self.getElementsByTagName(selector);
continue;
}
var splitter = splitters[i - 1];
var tagid = Selectors.Utils.parseTagAndID(selector);
var tag = tagid[0], id = tagid[1];
if (i == 0){
items = Selectors.Utils.getByTagAndID(self, tag, id);
} else {
var uniques = {}, found = [];
for (var j = 0, k = items.length; j < k; j++) found = Selectors.Getters[splitter](found, items[j], tag, id, uniques);
items = found;
}
var parsed = Selectors.Utils.parseSelector(selector);
if (parsed){
filtered = [];
for (var m = 0, n = items.length; m < n; m++){
item = items[m];
if (Selectors.Utils.filter(item, parsed, local)) filtered.push(item);
}
items = filtered;
}
}
return items;
};
})();
Everything this snippet does is to overwrite the selector functionality on the deepest level in MooTools, check for querySelectorAll or getElementsByClassName and it uses those methods for simple selectors whenever possible.
If you experience any bugs please notify me, you can usually fix them by just taking out this piece of code and put it back in once I fixed it (if it still doesn't work it is your fault). I have not yet experienced any problems with it. Please note that I have only enabled querySelectorAll for simple selectors like tag.class, #id, .class etc. For more complex selectors it still falls back to the MooTools selector engine. MooTools 2.0 will make this piece of code obsolete.
I'm not responsible for any side effects this script may causes. I'm using it and it works great, however it directly interferes with the MooTools-Core code which is generally not recommended. Please do not ask if we are going to include this into MooTools 1.2.x officially because we do not plan on changing the functionality as the branch is final and will only receive fixes.
Big thanks go out to digitarald who created the check for getElementsByClassName.
Still not enough? More fun with selectors: PseudoSelectors: extended
Leave a comment
11 comments on Speed Up Your Moo Part 1: Selectors
rpflo posted on 11th September 2009
Savageman posted on 30th August 2009
function new$() {
if (document.querySelector) return document.querySelector.apply(this, arguments);
else return old$.apply(this, arguments);
}
function new$$() {
if (document.querySelectorAll) return document.querySelectorAll.apply(this, arguments);
else return old$$.apply(this, arguments);
}
Does it achieve the same thing?
Viraj posted on 28th August 2009
Viraj
Garrick Cheung posted on 27th August 2009
window.addEvent('domready', function(){
var module = $('container');
if(!module){return false;}
var form = module.getElement('form'),
comments = module.getElements('li.comments');
/*... more code below ...*/
});
Love the comma separating vars too. So organized!
Garrick Cheung posted on 27th August 2009
var some_var = $('id_of_something').setStyle(..).addEvent(..);
The chain is your friend. The point of 'caching' by creating a variable is usually the usability of the object/element..whatever. Creating a variable related to $('id_of_something') allows me to reuse it if necessary, and it helps with readability and organization.
martin posted on 27th August 2009
$$('.whatever').set({ styles : {}, events : {}});
why is this considered nasty? Isn't this what mootools' set()-method is for?
On the point of "caching" I am still unsure what the impact is. I read that one should avoid doing
var bla = $('someId');
as this creates a circular reference that confuses the garbagecollector when the element in question is removed from the DOM but still has a reference in the javascript. I don't know how this plays with arrays of nodes ($$'s output) but I guess it has similar effects as a reference to a DOM-node is created (as part of the array) in memory. I don't even know if this all is still valid with current browsers but it might be something to consider when you are building apps which live more than a few minutes and create considerable amounts of nodes on the fly.
Chris posted on 27th August 2009
Dimitar: This code solely focusses on performance regarding selectors. You should not concern yourself with the speed difference of using
var elements = $$(..); elements.doSomething(); elements.doAnything() versus
$$(..).doSomething().doAnything()
try using console.profile() and console.profileEnd() to see it yourself. I don't think you will see a noticeable difference.
When querying selectors there is a lot going on, it first needs to parse the selector and then test nearly every element against it. That is a heavy operation and it gets faster the better you define what element you want to have.
tbela99 posted on 27th August 2009
Dimitar Christoff posted on 27th August 2009
the 1.2 setter:
$$("div.foo").set({
styles: {
marginTop: 10
},
events: {
click: function(e) {
}
}
});
... or chaining
$$("div.foo").setStyle('marginTop', 10).addEvent(...);
... thus avoiding the need to use variables to cache selectors (unless they will be needed later again)
any thoughts on how this would compare speedwise and memory wise (code readability aside)? I tend to avoid creating variables for anything that can be read dynamically if it's not going to be used more than once...
tbela99 posted on 27th August 2009
instead of doing
$$('.myElements').setStyle('marginTop', 10);
$$('.myElements').addEvent(...);
...
we'd better do
$$('.myElements').each(function (el) {
el.setStyle().addEvent()
...
});
if I'm not wrong the first code use two loops
procyon posted on 27th August 2009
Keep the good work, cheers,
Alexandre Rocha
$$('#sidebar .sidebarLinks').doSomething();
?