Better JavaScript Minification
Issue № 304

Better JavaScript Minification

In the past few years, performance research has become more prevalent thanks to research by the Yahoo! Exceptional Performance Team and Google’s Steve Souders. Most of that research studies not the individual HTML page, but the resources the page requires to display or behave correctly.

Article Continues Below

Although both CSS and JavaScript may be included within an HTML page, best practices encourage storing CSS and JavaScript in external files that can be downloaded and cached separately. Performance research asks: How can these external resources be downloaded and applied most efficiently? The first approach is to limit the number of external requests since the overhead of each HTTP request is high. The second approach? Make your code as small as possible.

The history of JavaScript byte savings#section1

Douglas Crockford introduced JSMin in 2004 as a way to shrink JavaScript files before placing them into a production environment. His simple tool removed spaces, tabs, and comments from JavaScript files, effectively decreasing the size compared to the original source file. His rationale was sound: decreasing the size of JavaScript code results in faster downloads and a better user experience.

Three years later, Yahoo! engineer Julien Lecomte introduced the YUI Compressor. The YUI Compressor’s goal was to shrink JavaScript files even further than JSMin by applying smart optimizations to the source code. In addition to removing comments, spaces, and tabs, the YUI Compressor also safely removes line breaks, further decreasing the overall file size. The biggest byte savings, though, come from replacing local variable names with one- or two-character names. For example, suppose you have the following function:

function sum(num1, num2) {
    return num1 + num2;
}

YUI Compressor changes this to:

function sum(A,B){return A+B;}

Note that the two local variables, num1 and num2, were replaced by A and B, respectively. Since YUI Compressor actually parses the entire JavaScript input, it can safely replace local variable names without introducing code errors. The overall function continues to work as it did originally since the variable names are irrelevant to the functionality. On average, the YUI Compressor can compress files up to 18% more than JSMin.

These days, it’s common to use a minification tool plus HTTP compression to further reduce JavaScript file size. This results in even greater savings than using either method alone.

Boosting minification#section2

A couple of years ago, as I started debugging large amounts of production code, I realized that the YUI Compressor didn’t apply variable replacement to a fairly significant portion of my code. Bothered by what I considered a lot of wasted bytes, I explored coding patterns to boost the YUI Compressor’s minification powers. I presented my results, Extreme JavaScript Compression with YUI Compressor, internally at Yahoo!.

In my investigation, I discovered coding patterns that prevented YUI Compressor from performing variable name replacement. By modifying or avoiding these coding patterns, you can improve the YUI Compressor’s performance.

JavaScript’s evil features#section3

Anyone who has followed Douglas Crockford’s writing or lectures knows about the “evil” parts of JavaScript: The parts that are confusing and/or that prevent us from writing clean code that performs well. The eval() function and the with statement are the two most egregious examples of evil JavaScript. Though there are other considerations, both of these features force YUI Compressor to stop replacing variables. To understand why, we need to understand the intricacies of how each works.

Working with eval()#section4

The eval() statement’s job is to take a string and interpret it as JavaScript code. For example:

eval("alert('Hello world!');");

The tricky part of eval() is that it has access to all of the variables and functions that exist around it. Here’s a more complex example:

var message = "Hello world!";function doSomething() {
    eval("alert(message)");
}

When you call doSomething(), an alert is displayed with the message, “Hello world!”. That’s because the string passed into eval() accesses the global variable message and displays it. Now consider what would happen if you automatically replaced the variable name message:

var A = "Hello world!";function doSomething() {
    eval("alert(message)"); 
}

Note that changing the variable name to A results in an error when doSomething() executes (since message is undefined). YUI Compressor’s first job is to preserve the functionality of your script, and so when it sees eval(), it stops replacing variables. This might not sound like such a bad idea until you realize the full implications: Variable name replacement is prevented not only in the local context where eval() is called, but in all containing contexts as well. In the previous example, this means that both the context inside of doSomething() and the global context cannot have variable names replaced.

Using eval() anywhere in your code means that global variable names will never be changed. Consider the following example:

function handleJSONP(object) {
    return object;
}function interpretJSONP(code) {
    var data = eval(code);
    
    //process data
}

In this code, pretend that handleJSONP() and interpretJSONP() are defined in the midst of other functions. JSONP is a widely used Ajax communication format that requires the response to be interpreted by the JavaScript engine. For this example, a sample JSONP response might look like this:

handleJSONP({message:"Hello world!"});

If you received this code back from the server via an XMLHttpRequest call, the next step is to evaluate it, at which point eval() becomes very useful. But just having eval() in the code means that none of the global identifiers can have their names replaced. The best option is to limit the number of global variables you introduce.

You can often get away with this by creating a self-executing anonymous function, such as:

(function() {
    function handleJSONP(object) {
        return object;
    }    function interpretJSONP(code) {
        var data = eval(code);
    
        //process data
    }
})();

This code introduces no new global variables, but since eval() is used, none of the variable names will be replaced. The actual result (110 bytes) is:

(Line wraps marked » —Ed.)

(function(){function handleJSONP(object){return object}function »
interpretJSONP(code){var data=eval(code)}})();

The nice thing about JSONP is that it relies on the existence of just one global identifier, the function to which the result must be passed (in this case, handleJSONP()). This means that it doesn’t need access to any local variables or functions and gives you the opportunity to sequester the eval() function in its own global function. Note that you also must move handleJSONP() outside to be global as well so its name doesn’t get replaced:

//my own eval
function myEval(code) {
    return eval(code);
}function handleJSONP(object) {
    return object;
}(function() {
    function interpretJSONP(code) {
        var data = myEval(code);
    
        //process data
    }
})();

The function myEval() now acts like eval() except that it cannot access local variables. It can, however, access all global variables and functions. If the code being executed by eval() will never need access to local variables, then this approach is the best. By keeping the only reference to eval() outside of the anonymous function, you allow every variable name inside of that function to be replaced. Here’s the output:

function myEval(code){return eval(code)}function handleJSONP »
(a){return a}(function(){function a(b){var c=myEval(b)}})();

You can see that both interpretJSON(), code, and data were replaced (with a, b, and c, respectively). The result is 120 bytes, which you’ll note is larger than the example without eval() sequestered. That doesn’t mean the approach is faulty, it’s just that this example code is far too small to see an impact. If you were to apply this change on 100KB of JavaScript code, you would see that the resulting code is much smaller than leaving eval() in place.

Of course, the best option is not to use eval() at all, as you’ll avoid a lot of hoop-jumping to make the YUI Compressor happy. However, if you must, then sequestering the eval() function is your best bet for optimal minification.

The with statement#section5

The with statement is the second evil feature that interferes with the YUI Compressor’s variable replacement technique. For those unfamiliar, the with statement is designed (in theory) to reduce the size of code by eliminating the need to write the same variable names over and over again. Consider the following:

var object = {
    message: "Hello, ",
    messageSuffix: ", and welcome."
};
object.message += "world" + object.messageSuffix;
alert(object.message);

The with statement allows you to rewrite this code as:

var object = {
    message: "Hello, ",
    messageSuffix: ", and welcome."
};
with (object) {
    message += "world" + messageSuffix;
    alert(message);
}

Effectively, the with statement avoids the need to repeat “object” multiple times within the code. But these savings come at a cost. First, there are performance implications from using the with statement, as local variables become slower to access. This happens because variables inside of a with statement are ambiguous until execution time: They may be properties of the with statement’s context object or they may be variables from the function or another execution context. To understand this ambiguity better, take a look at the code when the local variable message is added and the definition of object is removed:

var message = "Yo, ";with (object) {
    message += "world" + messageSuffix;
    alert(message);
}

When the identifier message is used inside of the with statement, it could be referencing the local variable message or it could be referencing a property named message on object. Since JavaScript is a late binding language, there is no way to know the true reference for message without completely executing the code and determining if object has a property named message. Witness how late binding affects this code:

function displayMessage(object) {
    var message = "Yo, ";    with (object){
        message += "world" + messageSuffix;
        alert(message);
    }
}displayMessage({ message: "Hello, ", messageSuffix: ", and welcome." });
displayMessage({ messageSuffix: ", and welcome." });

The first time that displayMessage() is called, the object passed in has a property named message. When the with statement executes, the reference to message is mapped to the object property and so the displayed message is, “Hello, world, and welcome.” The second time, the object passed in has only the messageSuffix property, meaning the reference to message inside of the with statement refers to the local variable and the message displayed is therefore, “Yo, world, and welcome.”

Since the YUI Compressor doesn’t actually execute the JavaScript code, it has no way of knowing whether identifiers in a with statement are object properties (in which case, it is not safe to replace them) or local variable references (in which case, it is safe to replace them). The YUI Compressor treats the with statement the same as eval(): when present, it will not perform variable replacement on the function or any containing execution contexts.

Unlike eval(), there is no way to sequester the with statement in such a way that it doesn’t affect most of the code. The recommendation is to avoid using the with statement at all. Even though it appears to save bytes at the time of code writing, you actually end up losing bytes by forfeiting YUI Compressor’s variable replacement feature. The displayMessage() function gets minified like this:

function displayMessage(object){var message="Yo, ";with(object) »
{message+="world"+messageSuffix;alert(message)}};

This result is 112 bytes. If the function is rewritten to avoid the with statement, displayMessage() looks like this:

function displayMessage(object) {
    var message = "Yo, ";    object.message += "world" + object.messageSuffix;
    alert(object.message);
}

When minified, this new version of the function becomes:

function displayMessage(a){var b="Yo, ";a.message+="world"+ »
a.messageSuffix;alert(a.message)};

The size of this result is 93 bytes, so even though the original source code is larger. The minified source code becomes smaller because we used variable replacement.

Conclusion#section6

YUI Compressor’s variable replacement functionality can give big byte savings while minifying your JavaScript. Since the YUI Compressor tries to avoid breaking your code by incorrectly replacing variable names, it will turn off variable replacement when the eval() function or with statement is used. These “evil” features alter how JavaScript code is interpreted and prevent the YUI Compressor from safely replacing variable names, which costs you a large amount of byte savings. Avoid this penalty by steering clear of eval() or sequester it away from the rest of your code. Also, avoid the with statement. These steps will ensure that your code doesn’t get in the way of optimal minification.

21 Reader Comments

  1. That is partially true. The Closure Compiler can result in smaller code by manipulating the underlying code structure, but it doesn’t do safe variable name replacement. In the presence of eval(), you could end up with broken code as it doesn’t turn variable replacement off. It also performs other optimizations that may result in unintended changes to your code’s functionality. While it shows promise, I’m not ready to recommend the Closure Compiler over the YUI Compressor.

  2. 1. eval and with are JavaScript features you should *generally* avoid. This is regardless of whether you are using any minification tool. Minification is quite possibly the *least* of the reasons why eval and with are evil.

    2. The following practices are similar to eval, and should be avoided for the same reasons:
    – using the Function constructor
    – passing strings to window.setTimeout or window.setInterval

    “why with is evil”:http://www.yuiblog.com/blog/2006/04/11/with-statement-considered-harmful/ and “why eval is evil”:http://xkr.us/js/eval

  3. Really nice article Nicholas. I can’t really get into the details of the usage of eval and with but i can easily say that, not only in Javascript but with any language you write your code in, optimizing the code is something any developer should do 🙂

  4. I can see some issues as well with the usage of eval and with, but i still concur that you have to minify your javascript somehow. It’s just a matter of choice imho.

    Nice article.

  5. Thanks for the comments, everyone. Just to clarify: the point of the article was not to simply say “avoid doing this,” but rather to give a clear description of *why* to avoid doing this (as I think everyone has heard “don’t use eval” and “don’t use with”, but rarely is there a nice description of why).

  6. Fantastic article and a great eye-opener. However, when most of the internet-using world could be on lightning fast connections within half a decade, is it still going to be such a huge deal to save a couple of kb here and there?

  7. Having smaller files and your website downoading faster can have SEO benefits, and this is meant to be more apparent since the Google Caffeine update, so keeping your code as lightweight as possible could be very useful in terms of ranking plus it could help with improving user experience.

    However I think it is always good to try and do things in the best possible manner.

  8. Keep in mind that lightning-fast connections are still bound by physical distance. The further the bytes have to travel, the longer it will take to get there, so if I request a byte from China it takes longer to receive than if I request that same byte from next door. Also, developing countries don’t have as much high speed internet penetration as the United States. There is still a significant portion of the world that accesses the internet over modem (or equivalent).

  9. No serious coder (most of the people that would be reading this article) has used either eval or with since the stone age. Reasons:

    # They introduce security vulnerabilities
    # They are slow _aside from compression_
    # They are difficult to debug

    One more reason is hardly worth an alistapart article, IMO.

    That being said, it was very well written.

  10. @Joss internet connections may be getting faster but there’s two very good reasons why you should be concerned with these sort of code optimisations:

    # Access to the Internet via mobiles is increasing at a much faster rate than for desktops, and file size is still a concern for most, not only in how long you have to wait, but in how much you have to pay.
    # Google has just announced that page speed will be included in their algorithm for search results, so page optimisation will also be an SEO factor.

  11. @Aaron – You would be surprised. There are definitely people still using “with”, but moreso eval(). Since JSON became popular, and native JSON parsing has only recently been introduced, most JavaScript libraries used eval(), and as such, didn’t get the best compression of their code.

  12. @Tasarım –

    bq. optimizing the code is something any developer should do

    I can’t agree with this — any developer should _be able_ to optimise code, but only when it is shown to be necessary. I completely agree with aaron and dsevil re the reasons not to use eval and with, but any kind of optimisation that reduces code readability/maintainability (and most do) should only be used when it has been proved that it is _essential_.

  13. The compressed code is not debuggable anymore, since all of it is in 1 line (JavaScript Error messages tell only the line number on which the error occurred).

    Google’s Closure Compiler has another problem in its maximum compression mode: it cannot recognize instances or properties which are dynamically addressed. Since Closure compiler does not detect its use, it will treat the instance/property as unused and remove its definition from the code.

    More than that, the code is not readable anymore without the use of some source beautifier or unpacker.

    In some cases, the minification leads to bigger compressed files – especially if one replaces window or document with a shorthand, because those cannot be compressed much further.

    BTW, it helps to keep the memory footprint of your JS small even without compression. Since there is a lack for very small Toolkits, it started one myself: http://tinyjs.sf.net – in the next version, I want to remove those selectors which are not very useful in normal cases to an external plugin.

  14. very nice and interesting article Nicholas – and thanks for telling us why not to use eval and with!

  15. This is just my two cents, but really, I’m surprised by how much this “minification/optimization makes debugging harder” argument still comes up. A simple one step build process which takes your CSS and JS and minifies/optimizes it isn’t that hard to come up with. I use a little Ruby script for that but you can even do it by hand! Just make sure you are using an scm tool like Git (in case you screw something up) and also that you have separate development and staging directories.

  16. As someone who is going to school for Graphic Design/Website Design. This is one of those articles that teaches you something that they don’t teach in school. This is something i’ll certainly keep in mind, when using JS. 🙂

  17. I recommend this site for anyone who likes playing in the bingo hall but likes online aswell!bingo is very crazy and popular game.It is a game of chance in which each player has one or more cards printed with differently numbered squares on which to place markers when the respective numbers are drawn and announced by a caller.I like to play bingo online….http://www.888enjoy.eu..

Got something to say?

We have turned off comments, but you can see what folks had to say before we did so.

More from ALA

Nothing Fails Like Success

Our own @zeldman paints the complicated catch-22 that our free, democratized web has with our money-making capitalist roots. As creators, how do we untangle this web? #LetsFixThis