Extreme JavaScript optimization

December 20th, 2009. Tagged: JavaScript, performance

2010 update:
Lo, the Web Performance Advent Calendar hath moved

Dec 20 This article is part of the 2009 performance advent calendar experiment. Today's article is a second contribution from Ara Pehlivanian (here's the first).

There's a Belorussian translation provided by Patricia. Thanks!

Ara PehlivanianAra Pehlivanian has been working on the Web since 1997. He's been a freelancer, a webmaster, and most recently, a Front End Engineer at Yahoo! Ara's experience comes from having worked on every aspect of web development throughout his career, but he's now following his passion for web standards-based front-end development. When he isn't speaking and writing about best practices or coding professionally, he's either tweeting as @ara_p or maintaining his personal site at http://arapehlivanian.com/.

There's an odd phenomenon underway in the JavaScript world today. Though the language has remained relatively unchanged for the past decade, there's an evolution afoot among its programmers. They're using the same language that brought us scrolling status bar text to write some pretty heavy duty client-side applications. Though this may seem like we're entering a Lada in an F1 race, in reality we've spent the last ten years driving an F1 race car back and forth in the driveway. We were never using the language at its full potential. It took the discovery of Ajax to launch us out of the driveway and onto the race track. But now that we're on the track, there's a lot of redlining and grinding of gears going on. Not very many people it seems, know how to drive an F1 race car. At least not at 250 mph.

The thing of it is, it's pretty easy to put your foot to the floor and get up to 60 mph. But very soon you'll have to shift gears if you want to avoid grinding to a halt. It's the same thing with writing large client-side applications in JavaScript. Fast processors give us the impression that we can do anything and get away with it. And for small programs it's true. But writing lots of bad JavaScript can very quickly get into situations where your code begins to crawl. So just like an average driver needs training to drive a race car, we need to master the ins and outs of this language if we're to keep it running smoothly in large scale applications.

Variables

Let's take a look at one of the staples of programming, the variable.Some languages require you to declare your variables before using them, JavaScript doesn't. But just because it isn't required doesn't mean you shouldn't do it. That's because in JavaScript if a variable isn't explicitly declared using the 'var' keyword, it's considered to be a global, and globals are slow. Why? Because the interpreter needs to figure out if and where the variable in question was originally declared, so it goes searching for it. Take the following example.

function doSomething(val) {
    count += val;
};

Does count have a value assigned to it outside the scope of doSomething? Or is it just not being declared correctly? Also, in a large program, having such generic global variable names makes it difficult to keep collisions from happening.

Loops

Searching the scope chain for where count is declared in the example above isn't such a big deal if it happens once. But in large-scale web applications, not very much just happens once. Especially when loops are concerned. The first thing to remember about loops, and this isn't just for JavaScript, is to do as much work outside the loop as possible. The less you do in the loop, the faster your loop will be. That being said, let's take a look at the most common practice in JavaScript loops that can be avoided. Take a look at the following example and see if you can spot it:

for (var i = 0; i < arr.length; i++) {
    // some code here
}

Did you see it? The length of the array arr is recalculated every time the loop iterates. A simple fix for this is to cache the length of the array like so:

for (var i = 0, len = arr.length; i < len; i++) {
    // some code here
}

This way, the length of the array is calculated just once and the loop refers to the cached value every time it iterates.

So what else can we do to improve our loop's performance? Well, what other work is being done on every iteration? Well, we're evaluating whether the value of i is less than the value of len and we're also increasing i by one. Can we reduce the number of operations here? We can if the order in which our loop is executed doesn't matter.

for (var i = 100; i--; ) {
    // some code here
}

This loop will execute 50% faster than the one above because on every iteration it simply subtracts a value from i, and since that value is not "falsy," in other words it isn't 0, then the loop goes on. The moment the value hits 0, the loop stops.

You can do this with other kinds of loops as well:

while (i--) {
    // some code here
}

Again, because the evaluation and the operation of subtracting 1 from i is being done at the same time, all the while loop needs is for i to be falsy, or 0, and the loop will exit.

Caching

I touched briefly on caching above when we cached the array length in a variable. The same principle can be applied in many different places in JavaScript code. Essentially, what we want to avoid doing is sending the interpreter out to do unnecessary work once it's already done it once. So for example, when it comes to crawling the scope chain to find a global variable for us, caching it the reference locally will save the interpreter from fetching it every time. Here, let me illustrate:

var aGlobalVar = 1;
 
function doSomething(val) {
    var i = 1000, agv = aGlobalVar;
    while (i--) {
        agv += val;
    };
    aGlobalVar = agv;
};
 
doSomething(10);

In this example, aGlobalVar is only fetched twice, not over a thousand times. We fetch it once to get its value, then we go to it again to set its new value. If we had used it inside the while loop, the interpreter would have gone out to fetch that variable a thousand times. In fact, the loop above takes about 3ms to run whereas if avg += val; were replaced with aGlobalVar += val; then the loop would take about 10ms to run.

Property Depth

Nesting objects in order to use dot notation is a great way to namespace and organize your code. Unforutnately, when it comes to performance, this can be a bit of a problem. Every time a value is accessed in this sort of scenario, the interpreter has to traverse the objects you've nested in order to get to that value. The deeper the value, the more traversal, the longer the wait. So even though namespacing is a great organizational tool, keeping things as shallow as possible is your best bet at faster performance. The latest incarnation of the YUI Library evolved to eliminate a whole layer of nesting from its namespacing. So for example, YAHOO.util.Anim is now Y.Anim.

Summary

These are just a few examples of how to improve your code's performance by paying attention to how the JavaScript interpreter does its work. Keep in mind though that browsers are continually evolving, even if the language isn't. So for example, today's browsers are introducing JIT compilers to speed up performance. But that doesn't mean we should be any less vigilant in our practices. Because in the end, when your web app is a huge success and the world is watching, every millisecond counts.

Tell your friends about this post: Facebook, Twitter, Google+

23 Responses

  1. This code:

    for (var i = 100; i–; ) {
    // some code here
    }

    Will execute 101 times instead of 100 times. You either want to start at 99, or use –i if you want it to run 100 times.

    Also, it will be 50% faster only if the loop doesn’t do anything. It’s the kind of premature optimisation that we shouldn’t be recommending. It makes code less readable for new programmers and doesn’t buy you enough benefit.

  2. Philip: I agree that it’s not as easy to read for new programmers. In fact, it isn’t easy to read for seasoned programmers. But this is about *extreme* performance optimization, and that kind of thing is rarely pretty.

    You’re right about the loop count, though I never state that I want the loop to run only 100 times ;-)

    As for premature optimization, I suppose I should have put up a warning about that, though I was writing this under the assumption that those who would be using these techniques are writing code that needs it (hence the references to large client-side web apps). But you are correct about not optimizing prematurely.

  3. Philip that loop is correct … for(var length = arr.length; length–;){…} if arr has length 3 the for will be executed with length === 2, length === 1, and length === 0, since the length– when it’s 0 will set length as -1 but will return the zero value which means the statement is false.
    The “new programmers” problem is quite a nonsense, if they are new, they need to understand the abc of the language included loops and scopes – otherwise they are not programmers. for best performances I still prefer in any case this:
    var length = arr.length; while(length) {….; –length;} there is an old test page about this stuff in any case: http://devpro.it/examples/loopsbench/

  4. Andrea, you are right about the loop being correct, but I’m going to side with Philip on the use of simpler forms of loop iteration for maintainability, regardless of how new the programmer is. Caching length is worthwhile, but further optimizations detract from the legibility of the code. When reading the code, the important part is the loop block contents. Doing anything out of the ordinary inside the loop setup will just cause the reader to pause to verify that it makes sense and is equivalent to a standard pattern.

    Per previous posts in the series, the most common performance vectors are related to DOM access, not simple js variable access and manipulation. If it comes down to a point where loop setup is a performance vector for your app, you’re either missing an architecture related vector or you are over optimizing. Let the js engine handle optimizations in loop setup under the hood and spare future generations the decryption.

    The post is appropriately named, and the link you provided is useful in that context.

  5. Andrea, you’re right. I made a mistake. I guess 20 years of C programming didn’t prepare me for this :)

    Ara, one more to add to your list (or many more if you like) is regular expressions…

    In general, avoid a regex replace when you can do a simple string replace. If you must use a regex replace, an anchored regex replace is much faster than a non-anchored one.

  6. The article is great! It doesn’t matter if every chunk of code is perfectly correct and how many iterations there are in the samples! It’s a great article about extreme optimization.

    Thanks!

  7. Hi, reading your post about extreme JS optimizations, I have to ask: should we use “–i” instead of “i–” ?

    I’m not an expert in JS, but it seems that, as in C, avoiding the use of “i–” form should be one of the easiest optimization to learn, because it implies the creation of the clone of the variable when the “–i” form doesn’t.
    To show the difference, running “var i = 1; alert(i–); alert(i);” will print 1, then 0,
    when “var i = 1; alert(–i); alert(i);” will print 0 and 0.
    Almost all posts about optimization I read use the “i–” form. That’s very confusing for me.

    And another question: I’m used to Ruby, and Rubyists usually don’t use “;” at the end of statements. Javascript also allows not using semicolons, and I always wondered if there’s an impact on performances.

    Thanks, Florent.

  8. About the previous comment, of course read “- -i” instead of “-i”, it seems that some formatting broke it ;)

  9. @Florent: Omitting the semicolons at line ends totally breaks your code when you try to minify/compress your code for faster downloading. It’s also considered very bad style and has no impact on performance.

  10. @jsq, thanks for the answer!
    I try hard not to forget semicolons, but sometimes some escape… ;)

  11. @Florent: profile your code before deciding whether to use --i or i--. This is true even in C. Modern compilers are pretty smart.

  12. it doesn’t matter the dummy example, just use it if you’ve a loop with hard data manipulation inside, i use this extreme optimization with an average of 400 object instances and it works perfectly reducing the exec time from 10secs to 1 second or less time, remember it’s “extreme optimization” not “extreme optimization for dummies”, don’t feed the troll.

    If you want to reduce the script time execution (with hard to read code) use this, it’s a optimization for execution not for reading.

    Good posts!

  13. Great post!

  14. I’ve done some messing about with this concept of the loop using a for loop and found that regardless of whether I define i (i.e. var i =0;) with the var keyword or not it still creates an i global variable that is also manipulated with the for loop. This was surprising to see and it contradicts everything I read about it being made a local variable in this case. I checked it over many times to make sure I didn’t a mistake but this is the case. This means that the interpreter goes outside of the loop every time it has to change the value of i. How do you handle this?

  15. Are these impove necessary for a normal website?

  16. Thanks for simplified instruction for JavaScript optimization. I am now more enthused to devote time and learn these techniques.

  17. Ara, great work, as usual. Appreciate the simplistic yet profitable approach to optimization. I have been playing with the recommended process and benchmarking to see just how much things can speed up, and when combined, there is up to a 15-20% performance increase when you get up into the millions of iterations.

    @Florent, you also have to be careful about simply switching the increment and decrement order dependent upon what you want your code to do. If you want the value to be evaluated BEFORE the increment/decrement is performed, you need to keep it after the variable name. Consider the following for an example:

    var i = 1, j = 1;
    alert([i, i++, i].join(‘,’));
    alert([j, ++j, j].join(‘,’));

    In this sample, the first will output 1,1,2 while the second will produce 1,2,2. Changing the order of the operator will change the evaluation order of the value as well. Switching this in code can very quickly become a hazard unless you are certain what the intended outcome is.

  18. Ugh. Code above should be (hopefully works this time):
    var i = 1, j = 1;
    alert([i, i++, i].join(','));
    alert([j, ++j, j].join(','));

  19. [...] a performance. Usar iframes em excesso então é um quase um crime. E há também as práticas de otimização de código e lógica JavaScript, como quebrar longas tarefas em pedaços menores com WebWorkers ou setTimeout, evitando bloqueio da [...]

  20. [...] a performance. Usar iframes em excesso então é um quase um crime. E há também as práticas de otimização de código e lógica JavaScript, como quebrar longas tarefas em pedaços menores com WebWorkers ou setTimeout, evitando bloqueio da [...]

  21. Surprise! It seems that my engine (V8) applies some kinda optimization
    to the classical «for» form, so it is actually FASTER than the optimized
    version. The tests I used are on jsperf.com/optimized-loop, so
    you can test them on your browser too.

  22. That’s weird. I changed the i=200 by i=199,
    and the optimized code is 25% faster now
    in Chromium (97000 vs 81000 ops/s).

    On Firefox, however, everything is slower (47000 ops/s)
    but the optimizations only cause a 2% of difference.

  23. [...] a performance. Usar iframes em excesso então é um quase um crime. E há também as práticas de otimização de código e lógica JavaScript, como quebrar longas tarefas em pedaços menores com WebWorkers ou setTimeout, evitando bloqueio da [...]

Leave a Reply