09.01
Below is the gzip algorithm in plain text, taken from http://www.gzip.org/. Why do we need to study it? Read below for the answer.
The deflation algorithm used by gzip (also zip and zlib) is a variation of LZ77 (Lempel-Ziv 1977, see reference below). It finds duplicated strings in the input data. The second occurrence of a string is replaced by a pointer to the previous string, in the form of a pair (distance, length). Distances are limited to 32K bytes, and lengths are limited to 258 bytes. When a string does not occur anywhere in the previous 32K bytes, it is emitted as a sequence of literal bytes. (In this description, `string' must be taken as an arbitrary sequence of bytes, and is not restricted to printable characters.) Literals or match lengths are compressed with one Huffman tree, and match distances are compressed with another tree. The trees are stored in a compact form at the start of each block. The blocks can have any size (except that the compressed data for one block must fit in available memory). A block is terminated when deflate() determines that it would be useful to start another block with fresh trees. (This is somewhat similar to the behavior of LZW-based _compress_.) Duplicated strings are found using a hash table. All input strings of length 3 are inserted in the hash table. A hash index is computed for the next 3 bytes. If the hash chain for this index is not empty, all strings in the chain are compared with the current input string, and the longest match is selected. The hash chains are searched starting with the most recent strings, to favor small distances and thus take advantage of the Huffman encoding. The hash chains are singly linked. There are no deletions from the hash chains, the algorithm simply discards matches that are too old. To avoid a worst-case situation, very long hash chains are arbitrarily truncated at a certain length, determined by a runtime option (level parameter of deflateInit). So deflate() does not always find the longest possible match but generally finds a match which is long enough. deflate() also defers the selection of matches with a lazy evaluation mechanism. After a match of length N has been found, deflate() searches for a longer match at the next input byte. If a longer match is found, the previous match is truncated to a length of one (thus producing a single literal byte) and the process of lazy evaluation begins again. Otherwise, the original match is kept, and the next match search is attempted only N steps later. The lazy match evaluation is also subject to a runtime parameter. If the current match is long enough, deflate() reduces the search for a longer match, thus speeding up the whole process. If compression ratio is more important than speed, deflate() attempts a complete second search even if the first match is already long enough. The lazy match evaluation is not performed for the fastest compression modes (level parameter 1 to 3). For these fast modes, new strings are inserted in the hash table only when no match was found, or when the match is not too long. This degrades the compression ratio but saves time since there are both fewer insertions and fewer searches.
In our quest for the best possible JavaScript file size, we have always tried to optimize our code for minification. BUT, better minification does not equate to better gzip compression. Here’s what you need to avoid to get a better gzip compression, do not munge strings.
Recently there have been some libraries trying this strategy: using array notation instead of the dot notation for members of Objects. Here is a perfect illustration.
// uncompressed var getElementById = 'getElementById'; document[getElementById].style.color = 'blue'; // minified var a='getElementById';document[a].style.color = 'blue';
And you have read gzip’s algorithm, right? What does it do? Well, it maintains a lookup table for strings (note: everything is treated as strings in gzip’s point of view). By munging strings, you are actually emulating gzip in some ways. Here’s what the code above just did, added ‘a’ to the gzip lookup table, instead of just having getElementById there.
I have created a very simple case of string munging. Here’s the proof.
In the uncompressed file I have the following iterated till line 1000.
'u1' 'u2' 'u3' 'u4' 'u5' 'u6' 'u7' 'u8' 'u9' 'u10'
In the munged file, I have the following iterated till line 1001
var a = 'u1', b = 'u2', c = 'u3', d = 'u4', e = 'u5', f = 'u6', g = 'u7', h = 'u8', i = 'u9', j = 'u10' a b c d e f g h i j
The results are, taken from here :
- uncompressed – 5,099 bytes
- munged – 2,103 bytes
- uncompressed gzipped – 93 bytes
- munged gzipped – 120 bytes
My experiments conclude the following, string munging is perfectly fine if you are munging less than 3 different unique strings and it yields better result minification-wise. Otherwise, leave the strings as is, and save yourself some bytes (gzipped). Suprised?
If this is true, then I’d say we’d get optimal results if we, as developers, munged our strings, but our minifiers rename the variables to (un-minifiable) strings already in use:
constructor, length, nodeName, script, break, slice, etc.
Would make the compressed source reallllly hard to follow but I think it’d lead to savings after gzip, yes?
More detail: The idea is to identify the javascript keywords that are in use and can be use legally as variable names (not reserved words or anything). Sort according to string size. Use those up and then go back to A, B, C, D, etc.
So:
Input:
(function(htmlelem,className){htmlelem[className]= htmlelem[className].replace(/\bnojs\b/,’js’)})(document.documentElement,’className’);
current output:
(function(B,C){B[C]=B[C].replace(/\bnojs\b/,’js’)})(document.documentElement,’className’);
proposed output:
(function(document,replace){document[replace]=document[replace].replace(/\bnojs\b/,’js’)})(document.documentElement,’className’);
Based on gzip’s technology I think this could be a measurable win for gzip-enabled agents. Thoughts?
I’m rather curious… have you guys heard of over-optimization?
You’re making your code much harder to work with (in terms of maintenance and initial authoring) to save (literally!) a few bytes.
This conversation is fascinating from a computer science perspective (I love thinking about this stuff, too), but the cost is incredibly high. If anything, the fruits of this research should appear as improvement to YUI Compressor and other minifiers, and *definitely* not as a guide for day-to-day javascript programmers.
@Craig I agree with you, we should sit back, relax and let gzip do its job. This will make the code more readable and you don’t have to do the extra work.
@Paul The optimization that you talked about can possibly be done but I predict that they will run into unforeseen problems if they do that.
Based on Ywg’s comments on Andrea’s post: http://webreflection.blogspot.com/2009/02/on-my-vicious-javascript-code.html
… I’m starting to believe the dictionary reduction wouldn’t be all that successful. However I would certainly think this should happen from within the YUI Compressor, and not manually.