Lessons in Javascript Optimization

I’ve been spending the last few days optimizing the Games homepage (the changes will probably hit your screens next week sometime, depending on how testing goes.)

The mission: Make the page (seem to) load faster.

I attacked it with Firebug and timed and profiled everything. I was a bit amazed by what I found. While I should have probably known all these points, I didn’t realize just how relevant they are. But numbers don’t lie, and they have a funny way of disabusing us of invalid assumptions.

1: Minimize HTTP Requests

The first and foremost optimization enhancement is to minimize HTTP requests. Concatenate all the javascript that the page needs into a single file, and just request that.

It’s lovely to think that your scripts are not the problem, because they’re so small, so efficient, and it’s important to load them as soon as they’re needed. That’s like thinking that other people’s hands have germs, but yours are clean. It’s hubris. If you have 10 scripts that are efficient and minimized, they will probably take about 10 times as long to download individually as if they were all concatenated.

I have been a proponent of including each script as it was needed so as to make modules active as soon as possible. Numbers don’t lie, and there’s no room for ego in programming. I had to bite the bullet on this one, and admit that it’s better to concatenate everything together and include it all at the end.

2: Javascript Runs Synchronously

The second issue is to be aware that Javascript makes the user wait unless you tell it not to. When the browser hits a script tag, everything stops until the script is done executing. If you have some code that takes a long time, and there’s no reason to make the user wait for it, don’t make the user wait for it! Either trim out the code so that it doesn’t take as long to run, or shunt it off into asynchronous mode by using setTimeout.

I wrote a whole post about using window.setTimeout to asynchronize your code, and it’s a great technique.

3: Profile before you Optimize

I don’t even know how much time I saved by using the profiler and timer tools in Firebug. By both timing and profiling the script file download, I found that it took about 3 seconds to download and execute the 10 script files, but a scant 800ms to run the code that they contained. The profiler showed me where the bottlenecks were, and by judicious use of window.setTimeout, I was able to cut that down to about 250ms.

In other words, about 70% of the javascript time was spent downloading files, and most of the rest was spent waiting for things that could have been done asynchronously. A lot of the things that I thought would be bottlenecks were fine, and there were a few that I never would have thought to look into. You can’t beat a really good profiler. I’d reckon that Joe Hewitt saved me about 10 hours this week alone.

4: Redraw Takes A Long Time

I didn’t even realize this before I started profiling, but every single one of my javascript bottlenecks were because I was doing something that involved either adding a DOM node or restyling something. Anything that changes anything about how the page looks will trigger a render pass and make the browser redraw the page. This takes time, and requires pieces that you can’t control, like the video memory and OS and so on.

Most of the time, you don’t need the user to wait. They won’t click the button (or whatever) until it shows up, so in the meantime, why not go on and do something else while it’s waiting? asynchronize that stuff with window.setTimeout, and architect your code so that you don’t depend on it being done at any particular time.

5: Other People’s Code Can’t Be Changed (but it can be moved)

In the real world, you often have to include components that other people wrote, and sometimes, they do things in their code that you’d rather they didn’t. In the real world, you still have to make your page work right, because passing the buck just doesn’t make the user experience any better. (And, in all fairness, in the real world, sometimes bad code comes out of good programmers. He might be forced for business reasons to use code that was written a very long time ago, or he might be a C++ pro who finds himself having to whip up some Javascript that was never really meant for prime time, or a million other possible reasons. Chances are, the programmer know it’s WTF-able, and would love to change it. I’ve been guilty of this, and you probably have, too.)

If you absolutely must, for whatever reason, include code that does a bunch of document.write, puts style tags into the body (triggering a redraw loop), or other bad practices, do it last. Put a placeholder element where the offensive code should go. Load your stuff. Put the offensive code at the bottom of the page in an invisible div. After it’s done its thing, remove the node that it’s in, and then put it where it belongs. It’ll still delay your overall load, but to the user, it will seem like everything loaded quickly except for that one module. This technique is almost like asynchronizing bottlenecks with setTimeout, from the user’s point of view.

6: with is faster than (function(){...

Please see the comments for more information. This advice is not accurate any more.

Normally, the with statement is a pretty bad idea. However, when used with an object literal, it can be a very handy way to create private variables. This:

  }) {
  // blah blah blah

is actually faster and more memory-efficient than this:

  var a=1, b=2;
  // blah blah blah

because the javascript compiler doesn’t have to create a whole new function object. It just pushes a single new object into the activation stack, and that’s it. Since that’s all that you need it to do, why ask for more?

Leave a Reply

Comments are moderated like crazy using a variety of plugins. There is a very high likelihood that your comment won't show up right away, especially if you have never commented here before, but it was not deleted.

Please be patient, and do not post your comment more than once. It will show up once it is approved.

You must be logged in to post a comment.