Peachpit Press

Optimizing JavaScript for Download Speed

Date: Apr 18, 2003

Return to the article

Learn how to put your JavaScripts on a low-char diet through techinques such as crunching and obfuscation, and also find the right balance between size and speed, or between features and responsiveness for your scripts.

A lightweight interpreted language, JavaScript is ideally suited to data validation, interactive forms, and enhancing navigation. Presented with such a broad toolset to play with, many authors have gone overboard with JavaScript, bulking up their sites at an alarming rate. Fortunately, JavaScript offers rich opportunities for file-size and execution-speed optimization. By using techniques like packing, compression, and obfuscation, you can realize 50 to 90 percent savings off the size of your JavaScript files. This chapter shows you how to put your JavaScripts on a low-char diet. In Chapter 10, "Optimizing JavaScript for Execution Speed," you will learn how to speed up the execution speed of your code.

Because JavaScript is part of web page content and not a standalone application, making your JavaScripts load quickly is important. The challenge is to find the right balance between size and speed, or between features and responsiveness.

When to Opt for Optimization

"The first principle of optimization is don't."1

Most JavaScripts are so fast and many are so small that they don't need to be optimized. First, code your scripts to work correctly and be self-describing by using the best algorithms and data structures you can. Then, if users start to notice a delay in loading or execution time, it's time to start thinking about optimization.

Larger, more complex scripts, such as cascading menus and expandable outlines, can benefit more from download speed than from execution speed optimization. Realistic interactive games and simulations can benefit more from execution speed than from file size optimization. As you'll discover in Chapter 10, you can trade size for speed complexity and vice versa. Optimizing both size and speed while maintaining legible code takes a combination of techniques.

Trim the Fat

JavaScript can benefit from many of the same optimization techniques used with HTML and CSS. Whitespace removal, crunching and obfuscation, file consolidation, and compression can all be used singly or in combination to shrink your scripts. Scripts can typically be reduced by 50 to 70 percent, while combining techniques on amenable files can yield savings of 80 to 90 percent, or a factor of 5 to 10 times smaller.

JavaScript offers more opportunities for optimization than (X)HTML and CSS because you can name your own variables, functions, and objects. The only names you can't abbreviate are the built-in statements and methods like document.getElementById().

As you've no doubt discovered, JavaScripts can become quite large, which can slow down the display of web pages. Any external scripts referenced in the head of your document must load before any body content displays. Even with smaller .css and .js files, it is important to minimize the number of HTTP requests as each one adds an indeterminate delay. The easiest way to optimize JavaScript is to trim the fat in the first place. Strive to add only features that will benefit users and save them time.

What About Legibility?

The knock against optimized scripts is that they are hard to read. This can be a good thing, because some authors want to obfuscate their code to make it difficult to copy and reverse engineer. However, optimized code is difficult to maintain and update. I recommend adopting a parallel strategy like the one you used for optimized HTML files. After you debug your code (which is fully commented and self-describing, right?), optimize it by hand or automatically optimize it to another file, like this:

script.js
script_c.js

Link to script_c.js for the added speed, and perform any edits on the original script.js, which you can then reoptimize.

Remove Whitespace

Well-written JavaScripts tend to have a lot of whitespace for better legibility. Indents, spaces, and returns make your code easier for you and others to read and modify; however, your browser doesn't care how pretty your code is. With a couple of exceptions, the JavaScript parser scans your script looking for tokens and ignores excess whitespace. So instead of this:

function printArray(a) {
  if (a.length == 0)
    document.write(" Array is empty");
  else {
    for (var i = 0; i < a.length; i++) {
      document.write(a[i] + " <br>");
    }
  }
}

Do this:

function printArray(a){
if(a.length==0)
document.write("Array is empty");
else{
for(var i=0;i<a.length;i++){
document.write(a[i]+"<br>");
}
}
}

Automatic Semicolon Insertion

The ECMAScript specification requires browsers to automatically insert semicolons where they are needed.2 Although they are optional, ending your lines with semicolons is good defensive programming practice. Semicolons are also required by most optimization strategies that remove excess whitespace. Without semicolons, removing whitespace can cause unintended consequences, such as running lines together. They also help programmers avoid errors. (Let's save programmers some time, too.)

Cut the Comments

Commenting your code is good programming practice, but comments take up valuable space. What's a programmer to do? There are two approaches to cutting comments: abbreviate your comments or remove them entirely. So instead of this (from PopularMechanics.com):

function gotoFinList() {

// "SAVE & FINISH" 
// this changes the bottom frameset to include a button to return to the homepage 
// it also submits the form in the main frame that will then generate a list of pages
// added during content editing.

Do this:

function gotoFinList() {

// chgs bottom frameset 2 incl button 2 ret 2 home 
// also submits form in main form and gen list of pgs
// added during content editg

Or even better:

function gotoFinList() {

You can use the parallel approach mentioned previously to keep a fully commented version for future updates.

Apply JavaScripts Wisely

JavaScripts can be included in an (X)HTML document in any of four ways:

Let's use our minimal Mondrian to demonstrate them all:

<!DOCTYPE html 
   PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd ">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Piet Mondrian's Home Page</title>
<script src="/scripts/foo.js" defer="defer" type="text/javascript"></script>
<script type="text/javascript">
   var foo = 1;
</script>
</head>
<body>
   <h1>Piet Mondrian: Abstract Impressionist</h1>
   <p>Mondrian was one of the great abstract masters...</p>
   <p><a onmouseover="window.status='Mondrian Home Page'; return true;"; 
href="http://www.mondrian.com">Mondrian.com</a></p> <p><a href="javascript:window.open('http://www.mondrian.com/')">Mondrian.com</a></p> </body> </html>

In honor of Mondrian and expediency, let's focus squarely on the first two options. Event handlers can be used to link behavior to elements (ideally with functions), but the javascript: pseudo-protocol should be avoided as the only reference of a link. The 11 percent of people without JavaScript won't be able to access this content.3 This is not exactly user friendly.4 A number of sites rely on JavaScript—even government sites that should be easily accessible like http://www.canadapost.ca/.

Here's the syntax of the script element:

<script>

Function:

Defines an executable script

Attributes:

CHARSET, DEFER, LANGUAGE (deprecated), SRC, TYPE

Examples:

<script type="text/javascript" src="/scripts/foo.js"></script> <script type="text/javascript" src="/f.js" defer="defer"></script> /* optimized */

End tag:

</script>, not optional

Alternate:

<noscript>alternate content</noscript>

Used in:

<head>, <body>


Minimize HTTP Requests

Like CSS, your JavaScripts should be designed to maximize speed by minimizing the number of HTTP requests they require. You can embed smaller JavaScripts within high-traffic pages to avoid an extra HTTP request (with caveats for XHTML, discussed in Chapter 5, "Extreme XHTML"), and for site-wide bandwidth savings, use external files. Group files where possible to minimize the overhead of additional file requests.

Upon first encounter, external scripts take one HTTP request per file. I've found CSS files are cached more reliably than JavaScript files, however. External JavaScripts can continue to spawn HTTP requests even after their first encounter.5 Unlike HTML objects (like images, Flash, and Java), which can be requested in parallel, the HTML parser must wait for the JavaScript interpreter to load and execute any JavaScript files before it can continue.

Defer or Delay Loading

Scripts are executed as they are loaded. You can have multiple non-overlapping script elements within an HTML document, both in the head and body. To maximize page-display speed, try to defer or delay loading your JavaScripts where possible. Every byte and HTTP request you put before your viewable content delays its display.

First introduced by Microsoft Internet Explorer 4, the defer attribute of the script element is now part of the HTML 4 and XHTML specifications. If your script does not produce any output, such as a function or array definition, you can use the defer attribute to give browsers a hint that they can defer execution until the rest of your HTML page loads. Here's an example:

<script src="/later.js" defer="defer" type="text/javascript"></script>
</head>

Try to design or rewrite your scripts to encapsulate code in functions that will execute onload. Then you can defer their execution and still include them in the head of your document. This technique has the added benefit of allowing external files to be compressed, because they are included within the head. You can execute defined functions onload, like this:

<body onload="later();">

Or you can avoid JavaScript errors in non-critical scripts that are triggered immediately (such as email validation or overlaid menus) by defining empty "stub" functions to be replaced by scripts that are redefined later:

<script>
<!--
function stub{};
// -->
</script>
</head>
<body>
...
<script src="/scripts/stub.js" type="text/javascript"></script> 
</body>

Be careful with this approach because larger or multiple external scripts can bog down the response of your page after it displays. As you learned in Chapter 1, "Response Time: Eight Seconds, Plus or Minus Two," you want to avoid slow response times after a page loads. This technique works for HTML, but for XHTML you'll need to eliminate the surrounding SGML comments through conditional logic for post-HTML-3.2 browsers.

Even better, for high-traffic pages, SSI or merge them into the page to save an HTTP request. Here's an example of merging a script at the end of a page:

<script type="text/javascript">
<!--#include virtual="newsticker.js" -->
</script>
</body>

We use this approach on WebReference.com's front page news flipper. We first embed a couple HTML news headlines within the flipper table, and then overlay these links with other stories with the delayed DHTML include. The main content displays first, and then the script loads—not vice versa. This technique gracefully degrades for folks without JavaScript. For more details, see http://www.webreference.com/headlines/nh/.

Delay Gotchas

There are some downsides to delaying external loading of JavaScript files. Moving larger or multiple external scripts to the end of the body just postpones the pain. Although your content may display faster, leaving your page can become slow and sticky.

Functions called earlier in the page will not be available until the external file loads. You can include empty functions in the head, or better yet, check to be sure that external files have loaded or a flag has been defined to avoid calling nonexistent functions.

Finally, compressed JavaScripts located outside the head are not reliably decompressed by modern browsers. Instead, move compressed scripts inside the head and use the defer attribute where possible.

Place Compressed .js Files in the head

Because of the way the original Sun JavaScript parser worked, decompression of compressed external JavaScript files works only when they are embedded in the head of (X)HTML documents. We'll unravel that mystery in Chapter 18, "Compressing the Web."

Conditionally Load External JavaScripts

You can conditionally load external JavaScripts into your (X)HTML files with languages like JavaScript, XSSI, and PHP. Rather than create one large monolithic file, some authors split their JavaScript code into separate files. A common technique is to use separate libraries for Internet Explorer 4+, Netscape 4+, and DOM-based browsers (ie4.js, ns4.js, and dom.js, respectively). Depending on what type of browser loads the page, the following JavaScript loads only the necessary code:

dom = (document.getElementById) ? true : false;
ns4 = (document.layers) ? true : false;
ie = (document.all) ? true : false;
ie4 = ie && !dom;

var src = '';
if (dom) src = '/dom.js';
else if (ie4) src = '/ie4.js';
else if (ns4) src = '/ns4.js';
document.write("<scr" + "ipt src=" + src + "><\/scr" + "ipt>");

This simple browser sniffer classifies browsers into four categories:

You'll learn more advanced compatibility techniques that can save HTTP requests in Chapter 17, "Server-Side Techniques."

Abbreviate and Map

Another JavaScript optimization technique you can use to crunch your code is abbreviation and mapping. Abbreviation in JavaScript is more flexible than in HTML. In JavaScript, you can name your variables, functions, and objects anything you want, but HTML requires a fixed set of tag names, although class and id names can be abbreviated.

So instead of this:

function validateParseAndEmail()
var firstButton

Do this:

function email()
var button1

Or even better:

function e()
var b

Here's an example from WebReference.com's home page. Peter Belesis' original dual "news flipper" was self-describing (http://www.webreference.com/headlines/nh/). It was also over 6.7K and slowed down our home page.

So instead of this (single-feed version):

<script src="/scripts/newsflipper.js">
</head>

...(newsflipper.js file below)
arTopNews=[];

for(i=0;i<arTXT.length;i++) {
  arTopNews[arTopNews.length] = arTXT[i];
  arTopNews[arTopNews.length] = arURL[i];
}

TopPrefix=prefix;

function NSinit() {
  fad1 = new Layer(119);
  pos1 = document.anchors['pht1'];
  pos1E = document.images['phb1'];
  fad1.left = pos1.x;
  fad1.top = pos1.y;
  fad1.clip.width = 119;
  fad1.clip.height = pos1E.y-fad1.top;
  fad1.bgColor = "#ffed9a";
  fad1.onmouseover = FDRmouseover;
  fad1.onmouseout = FDRmouseout;
}
if (IE4) {
  IEfad1.style.pixelHeight=IEfad1.offsetHeight;
  IEfad1.onmouseover=FDRmouseover;
  IEfad1.onmouseout=FDRmouseout;
}

function FDRmouseover() {
  clearInterval(blendTimer);
}...

We manually abbreviated and optimized the code by over 50 percent to less than 3K and used SSI to add it to the end of the page, like this:

<!--#include virtual="/f.js" -->
</body>

... (f.js file below)
aTN=[];for(i=0;i<arTXT.length;i++){aTN[aTN.length]=arTXT[i];aTN[aTN.length]=arURL[i];}
tP=prefix;
function Ni(){fad1=new Layer(119);
dI=document.images;dA=document.anchors;
pos1=dA['pht1'];pos1E=dI['phb1'];fad1.left=pos1.x;fad1.top=pos1.y;fad1.clip.width=119;
fad1.clip.height=pos1E.y-fad1.top;fad1.bgColor="#ffed9a";
fad1.onmouseover=Fmv;fad1.onmouseout=Fmo;}
function Fmv(){clearInterval(bT);}...

Moving the script from an external file in the head to an SSI at the end of the page saves one HTTP request, displays the body content sooner, and raises relevance. Of course, these terse abbreviations can be hard to read. One solution is to create a map of names and their abbreviated counterparts either manually or automatically with an optimization program (such as index.jsmap). Here's an example:

email  e
button1 b

I have yet to see an optimizer that can abbreviate to user-defined maps (although I'm told some are in development). Some optimizers abbreviate variables and objects automatically, but you're stuck with what the program chooses.

Crunching and Obfuscation

Not to be confused with compression, crunching (or crushing or packing) is a term programmers have adopted to describe removing excess to reduce code to a minimum size. Although you can manually crunch by removing whitespace, comments, and abbreviating, automated programs are a more practical option for larger projects. There are several JavaScript crunchers available, including these:

These programs all work the same way, removing whitespace and comments to compact your code. Some, like ESC, optionally abbreviate object and variable names.

Obfuscation Anyone?

Because JavaScript is an interpreted language, hiding your scripts is impossible. You can, however, make them more difficult to decipher. Crunching certainly makes your code more difficult to read. But for some this is not enough. That's where obfuscators come in. Code obfuscators substitute cryptic string tokens and scramble names to make your code virtually unintelligible but still functional.

Blue Clam: JavaScript Obfuscator

By the time you read this, Solmar Solutions, Inc., will have released Blue Clam, a Java-based JavaScript obfuscator designed to protect your intellectual property and optimize JavaScript files. In development for two years, Blue Clam includes features not found in other JavaScript obfuscators, including recursive directory tree parsing, a user-defined keyword dictionary, variable-length obfuscated keyword support, extended file types (such as .js, .jsp, and .asp), and a graphical environment. For more information, see http://www.solmar.ca.

All obfuscators work in a similar way to transform your program internally while preserving the same external functionality. One common obfuscation is to substitute short meaningless sequences like "cq" for longer descriptive names like "setAvatarMood." Let's look at some real-world obfuscated code. So this:

function setAvatarMood(theMood) {
 try {
  //see if we have to reset the mood's duration
  var resetMoodDuration = ((theMood != null) && (theMood != 'anim'));

  //make sure there is a mood
  if (!theMood) theMood = avatar.data.mood;

  //store the new mood in the avatar
  if (theMood != 'anim') avatar.data.mood = theMood;

  //see if the mood exists
  if (!globals.moods[theMood]) theMood = globals.defaultMood;

  //set the appropriate mood-image to visible and all others to invisible
  //by moving them in or out of view
  for (var aMood in globals.moods) {
   avatar.labeledElements['avatar' + globals.moods[aMood] + 'Image'].style.top = 
    ((aMood == theMood)?0:-10000) + 'px';

  }

  //let the mood expire if it is not equal to the default mood
  if (resetMoodDuration && (theMood != globals.defaultMood)) {
   if (theMood != 'anim') delayedEval(avatar.id + ".setMood", null);
   delayedEval(avatar.id + ".setMood", "try { engine.getAvatarByID('Quek', '" + 
     avatar.id + "').setMood('" + globals.defaultMood + "');
      } catch(e){;}", avatar.MOODDURATION);
  }
 } catch(e){}
} 

Becomes this (without whitespace removal):

function cq(de) {
 try {
  var ch = ((de != null) && (de != 'anim'));
  if (!de) de = kj.data.hu;
  if (de != 'anim') kj.data.hu = de;
  if (!io.uy[de]) de = io.we;
  for (var ty in io.uy) {
   kj.op['avatar' + io.uy[ty] + 'Image'].style.top = ((ty == de)?0:-10000) + 'px';
  }
  if (ch && (de != io.we)) {
   if (de != 'anim') qw(kj.id + ".pw", null);
   qw(kj.id + ".setMood", "try { po.pp('Quek', '" + kj.id + "').pw('" + io.we + "');
      } catch(e){;}", kj.ua);
  }
 } catch(e){}
} 

Even better (with whitespace removed):

function cq(de){try{var ch=((de!=null)&&(de!='anim'));if(!de)de=kj.data.hu;
if(de!='anim')kj.data.hu=de;if(!io.uy[de])de=io.we;for(var ty in io.uy){
kj.op['avatar'+io.uy[ty]+'Image'].style.top=((ty==de)?0:-10000)+'px';}
if(ch&&(de!=io.we)){if(de!='anim')qw(kj.id+".pw",null);
qw(kj.id+".setMood","try{po.pp('Quek', '" +kj.id+"').pw('"+io.we+"');} catch(e){;}
     ",kj.ua);}}catch(e){}} 

Without a map, these internal transformations make your program extremely difficult to reverse engineer, plus it's 65 percent smaller (from 1,091 to 376 characters). The code is part of Quek (http://www.quek.nl), a browser-based surf/animate/chat application written in JavaScript. This function changes the mood of an avatar. Lon Boonen of Q42 (http://www.q42.nl) obfuscates his JavaScripts to prevent prying eyes with a home-grown script and some manual tweaking. Thanks to Lon Boonen for these snippets.

JavaScript Obfuscators

JavaScript obfuscators are few and far between. Here are some examples:

You can go further and substitute extended ASCII characters to obfuscate and tokenize your code even more. For maximum confusion, obfuscate reserved words by breaking them up into strings and use a concatenated variable. So instead of this:

bc.getElementById = kj;

Do this:

jh='ge';kl='tEleme';oi='ntB';zy='yID';ui=jh+kl+oi+zy; bc[ui]=kj;

Self-Extracting Archives

Some extreme programmers have gone so far as to create their own self-extracting archives. Trading time for space, they store their encoded script into one long string by substituting shorter tokens for longer repeated strings. Tack on a small decompressor at the end to replace the tokens with the original strings and eval the decompressed code and voilá!—a self-extracting script.

These self-extracting archives take longer to decompress and execute, but download much faster. Some 5K contestants (http://www.the5k.org/) have adopted this approach to squeeze the maximum functionality into as little space as possible.

Fans of Chris Nott's 1K DOM API used a similar technique to reduce his tiny API to 634 bytes. Chris has automated the process with his compression utilities at http://www.dithered.com/experiments/compression/.

Compression ratios average about 25 percent for 5K files and higher for larger files. Because the decompressor adds about 130 bytes, smaller files actually can become larger. Nott recommends using files over 500 bytes for his client-side compression scheme.

Chris Johnson's Extended ASCII JavaScript Packer substitutes single byte-token extended ASCII characters for longer strings for efficient packing of JavaScripts (http://members.optusnet.com.au/~kris_j/javacomp.html).

With both of these programs, there are reserved letters and techniques that you must avoid to make them work. For the 5K contest, only client-side techniques are allowed. For most sites, server-side compression is a more practical solution.

JavaScript and Compression

JavaScript files are highly compressible, in some cases by as much as 60 to 80 percent. Modern browsers can decompress JavaScripts either in external files or embedded within (X)HTML files. As Chapter 11, "Case Study: DHTML.com," shows, the difference in size and speed can be dramatic. You can compress JavaScript files in two different ways: proprietary and standards-based.

Each browser has its own proprietary way of compressing JavaScripts, related to signed scripts, Java archives, or help file systems. In theory, you could create a sophisticated sniffer to load the appropriate file for the visiting browser, but you'd have to maintain four separate files. A cleaner way is to use standards-based gzip content encoding.

Like HTML, external JavaScripts can be delivered compressed from the server and automatically decompressed by HTTP 1.1-compliant browsers. The only gotchas to watch out for are that external compressed JavaScript files must be referenced within the head element to be reliably decompressed by modern browsers, and Explorer 5 has a subtle onload bug with compressed scripts. You can work around both gotchas, however. You'll learn all the details in Chapter 18, "Compressing the Web."

By grouping external JavaScripts and using compression, you can dramatically reduce their impact on page display speed and bandwidth usage.

Summary

The easiest way to optimize JavaScript is to avoid the need for it in the first place. First, trim the fat by deleting unnecessary scripts and features. To avoid being featured in an accessibility story, use JavaScript only to enhance the user experience, not to create it. Email validation, responsive pop-up menus, and more can be created without the need for optimization. For larger projects where size or speed is an issue, optimization makes more sense.

Because JavaScript must be downloaded, make sure that your code is optimized for size and grouped to minimize HTTP requests. Many a site has added fancy navigational gizmos that are slow to load, because of many small (and not so small) files, each taking an indeterminate amount of time to download. Embed shorter scripts on high-traffic pages for maximum speed.

Automated packing programs can remove white space and comments, and some can abbreviate names. For maximum control, however, abbreviate your object and variable names manually. Where possible, defer program execution and loading, and compress larger scripts located in the head. Here's a list of the techniques you learned in this chapter:

Further Reading

1301 Sansome Street, San Francisco, CA 94111