Data Behaviors — jQuery vs. querySelectorAll

Data Behaviors — jQuery vs. querySelectorAll

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.