A while back I posted a link to the awesome DateJS library on my JavaScript page. Around that time I also looked at the code to see if there was any way to reduce the file size and possibly tweak the performance a little...
Geoffrey McGill from Coolite (where DateJS was developed) asked me if I'd blog about some of my findings but I never got round to finishing my post - so here it finally is, mostly compiled from emails that were exchanged at the time.
Performance
I'll start with this as it's the shortest topic in the blog and one that will possibly freak some people out. It's a trick I used back in the days of ActionScript 5 (slow performance) programming to get both surprising performance gains and also reduce the amount of typing I was doing!
A nice trick
See if you can spot the performance hit with the following code:
Date.prototype.clone = function () { ... };
Date.prototype.compareTo = function (date) { ... }
Date.prototype.equals = function (date) { ... }
Date.prototype.between = function (start, end) { ... }
Date.prototype.addMilliseconds = function (value) { ... }
It's a trick question, there are actually two performance hits, both of which we can quickly eradicate:
- The first is that each time you reference Date that global object has to be located.
- The second is that each time you reference the .prototype object on Date, you're forcing the JS engine to go find .prototype.
So, how do we fix this? Simple:
var DP = Date.prototype;
DP.clone = function () { ... };
DP.compareTo = function (date) { ... }
DP.equals = function (date) { ... }
DP.between = function (start, end) { ... }
DP.addMilliseconds = function (value) { ... }
Because DP is a local variable, it's faster to reference. Because you're not looking for the .prototype object repeatedly, it's faster still. As an added bonus, you generally type fewer characters and the file size also shrinks.
Give it a try - you'll be surprised, especially on big libraries.
Here's what Geoffrey says:
Your original suggestion re: var DP = Date.prototype made a big impact on the size (and performance) of the library. ....just changing to holding a reference to the Date.prototype, we shaved approx 1.2k off the total library size. That translates to ~4.5% size reduction. And, it's faster!
You can apply the technique to all sorts of things. You could argue that it can somewhat obfuscate your code, which it probably would if you use it excessively, however it should be possible for automated tools to implement the optimisation at build/runtime leaving your development code unaltered.
Running backwards (might be out of date)
Another trick I used to use was, where possible, iterating through arrays backwards using a while loop. For example:
var x = someArray.length;
while (-1<--x) {
}
The variable x is used for aesthetic purposes, (-1<--x) just looks sweet IMHO, there's a "backwards flow" to it which I find strangely relaxing.
This was generally quicker than most other types of loop back in the day, but I've not had chance to test it against modern JS engines - if you test it, please let me know the results!
Shrinking the file size
My next mission was to see if I could shrink the file size of DateJS. I started with core-debug.js from the following URL:
http://datejs.googlecode.com/svn/trunk/src/
As the files will have changed a fair bit since then, I've attached them to this post.
1. Manual Filesize Reduction
I edited the file to replace some verbose repetitive code with while loops and also created a shortcut to Date.prototype. Diff the files to see what's changed. This resulted in core-guy.js:
Result: New version is a bit smaller
NB: I've not tested the core-guy.js version so it could contain bugs - if there are any they should be easy to fix.
2. JSMIN vs. /Packer/ (No, it's not what you think, don't jump to conclusions!)
Before instantly assuming /packer/ is bad: THIS IS A LIKE FOR LIKE COMPRESSION - ie. /Packer/ doing the same sort of things as JSMIN, nothing more, no Base62 or variable shrinking.
I downloaded the core.js file which used JSMIN (so I renamed it to core-jsmin.js for clarity) and compared it to Dean Edwards's /packer/ at http://dean.edwards.name/packer/ using the most basic packing (whitespace, etc).
Result: Packer versions are smaller
Again, I've not checked the packer versions - it's quite strict on syntax but any bugs should be easy to fix.
3. Even smaller: /Packer/ + Shrink Variables - gzip friendly!
The /packer/ also has a "shrink variables" option, which isn't available in JSMIN as far as I know...
Result: Packing + Shrinking is even better
There's no performance hit - all it does is intelligently shrink internal variable names.
The packed + shrunk variables version is perfect for use with gzip compression (ie. gzip will make it even smaller). This has been proven by jQuery and many jQuery plugins - yet most developers think that using "Shrink Variables" will make the gzip compression less effective.
4. Super-small for non-gzip scenarios
For scenarios where gzip isn't available (for whatever reason) the /packer/ also has a Base62 encode option:
Obviously, the Base62 option does have a performance hit - anything up to 200ms while it "unpacks", but then again in scenarios where you can't use gzip shaving a few more Kb's off the file size will probably make up for that. Once the unpacking is done there's no further performance hit.
Whether you use my tweaked core or the original core, the /packer/ gives a much smaller file for non-gzip use.
And finally...
I also used CompressorRater (http://compressorrater.thruhere.net/) to check core-guy.js against various other compression methods and /packer/ still gave the best results.
Some tips for packer
Don't discredit it because of the bad things you've heard about Base62 compression - that's an optional feature that you'll very rarely need and should you need it, you'll be thankful it exists!
Always put a semicolon on a new line at the start of your script to allow it to be safely merged and packed with other scripts.
Always put a semicolon at the end in scenarios like the following:
var foo = function { ... }
var bar = function { ... };
Packer could chimp otherwise.