
In October 2012 I wrote an article for .Net magazine, “Get your JavaScript in order,” in which I detailed how I organise my JavaScript files and fire functions linked to DOM elements. The peer reviewer of my article, Ross Bruniges’, main concern was how the JavaScript searched the DOM because I had used the well known slowest method of searching through the page. Now I have an updated version that should put any doubts to rest: querySelectorAll.
At the time of the original article, I expected to be questioned on my methodologies but was confident in the validity of the writing as we (AREA 17) had successfully used this method on Unpakt, DLN and Facebook Stories. I went ahead and submitted the article and just after, Facebook Stories went through stress testing and a code audit by Engineyard engineers and the method stood up well.
(If you haven’t read that article and you are looking to organise your JavaScript, have a quick read then finish reading this and take away the code update here.)
Bruniges’ concern about my data-behaviors method was searching the DOM for data-behavior attributes; searching for ID’s, classes and tags is much faster as they use browser native methods. Searching for attributes requires parsing the DOM and can take much longer to do. Back in 2012 I wasn’t aware of a better way of finding these custom attributes so I trusted jQuery.
Old and slow:
$(document).find("*[data-behavior]").each(function(){ });
Enter querySelectorAll. This function has been around since IE8, Chrome 1, Firefox 3.5, Safari 3.2 and a bunch of Android versions. Notably there is no support for IE7 and below and IE8 will only accept CSS2 selectors.
New and fast:
document.querySelectorAll("[data-behavior]");
In various projects in which I’d used this method I had never noticed a performance hit when firing the data-behaviors with the jQuery method so I never sought out a quicker option. But recently, AREA 17 technical director, Luis Lavena, has been making noise about how much effort his team has been putting toward optimising the back end performance of our apps and so any performance gains on the front end would be nice. And then he mentioned Roberto Dip, a young Argentinian who was inspired by the data-behaviors and updated them in his Essential JS framework. This was comical to me, as I took the data-behaviors idea from Adam Berlin’s “Elemental JS” while working at Pivotal Labs with AREA 17 a few years ago and now it had inspired someone else to do something else. Importantly, Dip had run some performance tests to optimise how the behaviors get triggered and his work highlighted one crucial fact: querySelectorAll is much faster than jQuery.
This was surprising as I was under the impression that if there was a browser native method available, jQuery used that. For example $("#foo")
is shorthand for document.getElementByID("foo")
. But with attribute selectors, it clearly doesn’t do this.
Slow old method:
a17.LoadBehavior = function(context){
if(context === undefined){
context = $(document);
}
// find and iterate
context.find("*[data-behavior]").each(function(){
var that = $(this);
var behaviors = that.attr('data-behavior');
// try run each
$.each(behaviors.split(" "), function(index,behaviorName){
try {
var BehaviorClass = a17.Behaviors[behaviorName];
var initializedBehavior = new BehaviorClass(that);
}
catch(e){
// No Operation
}
});
});
};
And the fast new method:
a17.LoadBehavior = function(context){
if(context === undefined){
context = [document];
} else if (context.length == 0) {
return false;
}
for (var ci = 0, cl = context.length; ci < cl; ci++) {
// grab elements
var all = context[ci].querySelectorAll("[data-behavior]");
var i = -1;
// go through them
while (all[++i]) {
var currentElement = all[i];
var behaviors = currentElement.getAttribute("data-behavior");
var splitted_behaviors = behaviors.split(" ");
// loop behaviors and run
for (var j = 0, k = splitted_behaviors.length; j < k; j++) {
var thisBehavior = a17.Behaviors[splitted_behaviors[j]];
if(typeof thisBehavior !== "undefined") {
thisBehavior.call(currentElement,$(currentElement));
}
}
}
}
}
This new method works the same as the old, just a lot faster. A JS-Peformance test shows querySelectorAll working at least 4 times as fast (and often faster) in some browsers.
Like the original, you can pass in specific places for the function to look with the context parameter. In which case the new querySelectorAll method is even faster. The context argument will accept an array of elements so that if you are AJAXing content, you can load behaviors in multiple elements in one call. This was not something I’d originally forseen but something I noticed ESPN using in a couple of places during our work with them and it necessitated an extra loop.
Speaking of ESPN… I introduced data-behaviors to their dev team in Bristol, Connecticut and have adopted them throughout the project we’ve been working on and it seems they like them. These same data-behaviors are also being used on several of our other projects for clients like Billboard, Advertising Age, Filmmaker and the Webby Awards.
For now it looks like I’ll continue with this method until the benefits of a full JavaScript framework such as Backbone, Ember or Angular are thoroughly sold to me. I also like this update as its a step towards ditching using jQuery on projects altogether and saving vital KB’s of downloads for mobile sites. I’ll touch more on this in my next article.
Updated Thursday 17th July:
New method code sample includes a test to see if the behavior exists and a quicker way of triggering behaviors using Function.prototype.call() – new jsPerf test – now up to 8x faster than my original method.
Hi Mike,
First of all, great article and thanks for the mention.
I have a question, Why you are using a try / catch block instead of just checking the existence of the attribute?
If you use a try / catch block and you have an error during the behavior initialization you don’t get information about what is going on. The error is just silently ignored. This can make some errors really hard to track.
Essential checks if the type of the attribute ( in this case the behavior itself ) is undefined: https://github.com/roperzh/essential.js/blob/master/src/main.js#L83
The try/catch stops an error in an individual behavior stopping all the other behaviors from running and it mostly came from working with multiple FE devs with a continuous integration server at Pivotal. Checking to see if the behavior isn’t undefined is a good idea.
I’ve also been thinking about firing the events with .apply():
espn_ui.Behaviors[splitted_behaviors[j]].apply($(currentElement));
Seems faster:
http://jsperf.com/data-behaviors-jquery-vs-queryselectorall/2
http://jsperf.com/jquery-selector-by-name