DailyJS

Let's Make a Framework: hasClass Optimisation

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks lmaf optimisation

Let's Make a Framework: hasClass Optimisation

Posted by Alex R. Young on .
Featured

tutorials frameworks lmaf optimisation

Let's Make a Framework: hasClass Optimisation

Posted by Alex R. Young on .
*Let's Make a Framework* is an ongoing series about building a JavaScript framework from the ground up. These articles are tagged with [lmaf](http://dailyjs.com/tags.html#lmaf). The project we're creating is called [Turing](http://github.com/alexyoung/turing.js). Documentation is available at [turingjs.com](http://turingjs.com/).

Last week I explained how a simple hasClass implementation might work for detecting
CSS classes, complete with tests and suitable documentation. Henrik
Lindqvist
wrote a comment with some code that he
claimed was faster. If you're building your own open source project it's
likely that people may post their own performance suggestions and
patches. This should be managed with care, because overly aggressive
optimisation can potentially lead to confusing code or unexpected bugs.

In this tutorial I'm going to walk through Henrik's code as I would any
optimisation suggestion, using a little bit of science in the form of
tests and benchmarks.

The Original hasClass

The original code is based around a regular expression. Like most of my
tutorial code, I've attempted to make it extremely explicit and easy to
follow:

dom.hasClass = function(element, className) {
  if (!className || typeof className !== 'string') return false;
  if (element.nodeType !== nodeTypes.ELEMENT_NODE) return false;
  if (element.className && element.className.length) {
    return new RegExp('(^|\\s)' + className + '($|\\s)').test(element.className);
  } else {
    return false;
  }
};

The Optimised hasClass

This is Henrik's code:

function hasClassString(e, c) {
  var s = e.className, i = s.indexOf(c);
  return i != -1 && (s.charCodeAt(i - 1) || 32) == 32 && (s.charCodeAt(i + c.length) || 32) == 32;
};

The first line gets the class name from the element and finds the index
of the class name that we're looking for using indexOf.

The second line is longer. The comparison with -1 is done
as early as possible to optimise cases where the class name hasn't been
found. The next part checks to see if the character before the match is
a space (32 is the character code for space). When the index is outside
the string, charCodeAt will return 32 using the
|| because NaN will be returned:

''.charCodeAt(0)
// NaN

''.charCodeAt(0) || 32
// 32

'a'.charCodeAt(0) || 32
// 97

The same thing is true for the end of the line, which the part after
&& deals with.

It looks like this code makes sense, but is it really faster?

Benchmarking

To benchmark these functions, I used
Benchmark.js to compare the performance of each:

var Benchmark = require('benchmark')
  , suite = new Benchmark.Suite
  , div = { className: 'example1 example2 example3' };

function hasClassString(e, c) {
  var s = e.className, i = s.indexOf(c);
  return i != -1 && (s.charCodeAt(i - 1) || 32) == 32 && (s.charCodeAt(i + c.length) || 32) == 32;
};

function hasClassRegExp(element, className) {
  if (element.className && element.className.length) {
    return new RegExp('(^|\\s)' + className + '($|\\s)').test(element.className);
  } else {
    return false;
  }
};

suite.add('hasClassString', function() {
  hasClassString(div, 'example1');
  hasClassString(div, 'example2');
  hasClassString(div, 'example3');
  hasClassString(div, 'unknown');
})
.add('hasClassRegExp', function() {
  hasClassRegExp(div, 'example1');
  hasClassRegExp(div, 'example2');
  hasClassRegExp(div, 'example3');
  hasClassRegExp(div, 'unknown');
})
.on('cycle', function(event, bench) {
  console.log(String(bench));
})
.on('complete', function() {
  console.log('Fastest is ' + this.filter('fastest').pluck('name'));
})
// run async
.run(true);

The results on my machine look like this:

hasClassString x 2,319,886 ops/sec ±0.27% (81 runs sampled)
hasClassRegExp x 17,348 ops/sec ±0.39% (81 runs sampled)

The indexOf/charCodeAt version appears to perform over 100
times faster!

Testing

I dropped the optimised code into the DOM module:

/**
 * Detects if a class is present, optimised by Henrik Lindqvist.
 *
 * @param {Object} element A DOM element
 * @param {String} className The class name
 * @return {Boolean}
 */
dom.hasClass = function(element, className) {
  if (!className || typeof className !== 'string') return false;
  if (element.nodeType !== nodeTypes.ELEMENT_NODE) return false;
  var s = element.className, i = s.indexOf(className);
  return i != -1 && (s.charCodeAt(i - 1) || 32) == 32 && (s.charCodeAt(i + className.length) || 32) == 32;
};

Then I ran the tests in IE6, 7, 8, Firefox 4, Chrome, Safari, and gave
up because it seemed fine. That's not to say my tests are perfect,
however, but it seems Henrik's code does what we want.

Managing Optimisation

When it comes to optimising code, you're only as good as your benchmarks
and tests. There's also the question of whether optimisation is really
useful. In this case, hasClass is a good candidate for
optimisation because it's likely to be used frequently by client-side
developers. There are times where regular expressions won't perform as
well as direct string manipulation, but will yield more succinct code. I
found Henrik's code easy to follow and the performance improvement was
huge, so this seems clear cut to me.

The new code should have proper client-side benchmarks, but I'll save
that for another week.

This week's latest commit was
0ddf1c.

[Update] Reader Feedback

Ryan Cannon suggested this:

dom.hasClass = function(element, className) {
  return (' ' + element.className + ' ').indexOf(' ' + className + ' ') !== -1;
};

It's slightly slower than Henrik's suggestion, but it fixes a problem I
didn't spot that Adam Solove pointed out:

Given a div with the classes: "something some" and a query for the class "some", this code returns false when it should return true. The first match of a substring isn't followed with a space, but you need to repeatedly look for the same substring in case it occurs by itself later.

I forgot to include coverage of
element.classList again, so I've added that as well:

if (turing.detect('classList')) {
  dom.hasClass = function(element, className) {
    return element.classList.contains(className);
  };
} else {
  dom.hasClass = function(element, className) {
    return (' ' + element.className + ' ').indexOf(' ' + className + ' ') !== -1;
  };
}

Recall that turing.detect will cache the result and first
requires turing.addDetectionTest to work. In this case it's
only called once.

I've added the readers that suggested these improvements to the
contributor list in Turing's README.

References