Speed Up Your Moo Part 1: Selectors

filed under MooTools, posted on 27th August 2009 by Chris, 11 comments

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.

 


Download this file

/* 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

What about:

$$('#sidebar .sidebarLinks').doSomething();

?

Savageman posted on 30th August 2009

Why not Extending $$ and $ directly instead of Selectors.Getters? I'm curious to know if this (pseudo-code, I'm not guru yet) would work:

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

Nice post Chris, Keep up the good work. Waiting for your next post.

Viraj

Garrick Cheung posted on 27th August 2009

And it's much faster once you cache something. I usually have a "master" container that I need to get element(s) from.

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

I would usually do:

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

I go with dimitar's approach of

$$('.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

tbela99: you are right, .each is faster but I rarely use it. In fact, I have never found it to be a speed issue when just using the methods right on the collection despite the additional iterations that are necessary. Iterating over all elements in an array is not really costly. The code is shorter and more readable when avoiding the additional each.

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

alse do not abuse {link: "chain"} in your effects! if an effect is driven by a user action, a click for example, and he click just 10 times...the browser will become really slow

Dimitar Christoff posted on 27th August 2009

Or ... you could be nasty and use...

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

I think we should use each on Elements instance every time it is needed.

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

Great post Christoph, really good tips. Keep'em coming. Shame on me for using $$('.sidebarLinks').doSomething() sometimes!! The document.querySelectorAll code for Moo 1.2 it's a gem ;)
Keep the good work, cheers,

Alexandre Rocha
Styx PHP Framework Contact Follow me on Twitter