K.R.C. LogoThe Book of Kara

Speeding up Prototype's document.viewport.getDimensions

Published 4 October 2008

Hi! You've stumbled upon a blog post by a guy named Ryan. I'm not that guy anymore, but I've left his posts around because cool URIs don't change and to remind me how much I've learned and grown over time.

Ryan was a well-meaning but naïve and priviledged person. His views don't necessarily represent the views of anyone.

JavaScript has a lot of haters. They gripe about its loose typing and semicolon insertion. They whine about it's lack of inheritance and hardened classes. My guess os that many of these complaints come from programmers trying to write JavaScript as if it were Java, C or Objective-C. JavaScript, however, is a unique dynamic and expressive language, and this makes best suited for the Web.

Browsers are fickle beasts, and have some wildly different implementations of the same operations. At the same time, JavaScript applications are becoming more complex and the Internet is rapidly spreading to gaming consoles, mobile phones and linux-powered toasters. Performance optimization is an important tool to keep our applications running sprightly.

Prototype's document.viewport.getDimensions is a great example of some easy optimization. The method, in the master:

document.viewport = {
  getDimensions: function() {
    var dimensions = { }, B = Prototype.Browser;
    $w('width height').each(function(d) {
      var D = d.capitalize();
      if (B.WebKit && !document.evaluate) {
        // Safari <3.0 needs self.innerWidth/Height
        dimensions[d] = self['inner' + D];
      } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
        // Opera <9.5 needs document.body.clientWidth/Height
        dimensions[d] = document.body['client' + D]
      } else {
        dimensions[d] = document.documentElement['client' + D];
      }
    });
    return dimensions;
  }
  /* other methods */
};

The function operation is quite simple:

  1. Create an object to hold your results
  2. Create an array of possible properties
  3. Loop through those properties, each time:
  4. Figure out which method to use, based on which browser you have
  5. Get the viewport width using that method
  6. Figure out which method to use, based on which browser you have
  7. Get the viewport width using that method
  8. Return the object

However, it's very wasteful. Steps 2, 4 and 7 execute every time you run the method, even though their results will always be the same. This may not be a big deal, but if you're using this method a lot—for example, along with the window.onresize event handler which can fire many times per second—every little bit can help.

The above method uses some Ruby paradigms—$w() and self—leveraging a style unique to JavaScript will help us save some complexity without changing underlying logic.

document.viewport = {
  getDimensions: function() {
    var B, vpsizer;

    B = Prototype.Browser;

              // Safari <3.0 needs self.innerWidth/Height
    vpsizer = (B.WebKit && !document.evaluate) ?
      function (D) {
	    return self['inner' + D];
	  } :

      // Opera <9.5 needs document.body.clientWidth/Height
      (B.Opera && parseFloat(window.opera.version()) < 9.5) ?
        function(D) {
	      return document.body['client' + D];
	    } :
        function(D) {
	      return document.documentElement['client' + D];
	    };

    function it(acc, value) {
      acc[value] = vpsizer(value.capitalize());
      return acc;
    }

    function f() {
	  var r = {};
	  it(r, 'height');
	  it(r, 'width');
	  return r;
	}

    return (document.viewport.getDimensions = f)();
  }
  /* other methods */
};

This might look a little alien at first, so let me walk through it.

  1. Remove the array of properties. It's a waste to invoke all of Prototype's Array mechanics for a two-member array that never changes. We still set the browser name because we're doing the same checks as the previous function.
  2. We set vpsizer to a different function based on the browser's needs. While using the ternary operators and function literals together looks a little awkward, it's the most concise way to so set a variable based on shifting conditions where you can't use a switch statement. In my opinion it's always best to minimize the amount of assignment operators you use, as it becomes easier to track down exactly where variables are set.
  3. The two functions it (short for iterator) and f are our workhorses. Now that we've stored vpsizer in memory, we call it for 'width' and 'height'.
  4. In the last line we're doing three things in concert:
  5. Assigning document.viewport.getDimensions (the current method) to our workhorse function f. This means that all of the logic we've done so far remains in memory and we never have to do it again.
  6. The assignment operator also returns the assigned value. By wrapping the assignment in parenthesis, we can then use the () operator to immediately call f.
  7. We exit the function, returning the value of f().

What have we done? After all, the function looks more complex now than before. However, running the following script will show the performance difference:

alert(document.viewport.getDimensions);
document.viewport.getDimensions();
alert(document.viewport.getDimensions);

Note that second alert: document.viewport.getDimensions is now only four lines of code. We have, however, removed quite a bit of syntactical sugar. Much like putting a slinky on top of your stairs converts kinetic energy into potential energy, we've converted computational complexity into conceptual complexity.

I'm not a pro at testing performance, but a rudimentary test is showing performance gains of averaging around 40% or more over time. Re-setting document.viewport gave me gains of 15% or so, and removing the Array madness added 25%. Now a cynic might say that we're only really shaving a handful of milliseconds off of execution, but remember: checking the viewport size is not an intensive operation. Using these techniques in your own code can come to surprising benefits.

All that's left, of course, is to commit the code. Problem. Prototype Rake to do unit tests, and I can't for the life of me get Opera to pass any of the viewport tests. In fact, I can't get window.resizeTo() to work in Opera at all unless the window I'm resizing is a pop-up I've created. I've tried setting and unsetting Opera's JavaScript preferences in 9.5 and 9.2 multiple times. Any help would be appreciated.

Notes

  1. One note, however: do not use this technique to cache DOM Objects, lest you invoke the feebleness of [Internet Explorer][].
K.R.C. LogoPrevious: I make Web sites, ergoNext: Scope in ActionScript 3 is so fubared I can barely express it in words