📜 ⬆️ ⬇️

Himself gzip: compress scripts 20% better

Duplicate keys

If you look at the script compressed by Closure Compiler , om, YUI Compressor or something else, you can see endless rows of duplicate keys: .prototype , .length , offsetParent and so on. Let's try to get rid of them using the example of the jQuery UI Sortable plugin. I’ll say right away that we don’t surpass gzip, but when it’s not at hand or impossible to use it (for example, at the 10K Apart competition), this compression technique can be very useful.

Take the jquery.ui.sortable.js file version 1.8.2 as one of the most impressive in the jQuery UI bundle. The source code weighs 38.5 KB, compressed by the Closure Compiler, 23.1 KB, in gzip, 5.71 KB. In order not to clutter up the screens, we will choose some characteristic method and imagine that this is a separate plugin . I liked _mouseCapture() :
  1. / *!
  2. * Source
  3. * /
  4. ( function ($) {
  5. var
  6. _mouseCapture = function ( event , overrideHandle) {
  7. if ( this .reverting) {
  8. return false ;
  9. }
  10. if ( this .options.disabled || this .options.type == 'static' ) return false ;
  11. // We have to refresh the items data once first
  12. this ._refreshItems ( event );
  13. // Find out if it’s clicked node (or one of its parents)
  14. var currentItem = null , self = this , nodes = $ ( event .target) .parents (). each ( function () {
  15. if ($ .data ( this , 'sortable-item' ) == self) {
  16. currentItem = $ ( this );
  17. return false ;
  18. }
  19. });
  20. if ($ .data ( event .target, 'sortable-item' ) == self) currentItem = $ ( event .target);
  21. if (! currentItem) return false ;
  22. if ( this .options.handle &&! overrideHandle) {
  23. var validHandle = false ;
  24. $ ( this .options.handle, currentItem) .find ( "*" ) .andSelf (). each ( function () { if ( this == event .target) validHandle = true ;}));
  25. if (! validHandle) return false ;
  26. }
  27. this .currentItem = currentItem;
  28. this ._removeCurrentsFromItems ();
  29. return true ;
  30. };
  31. }) (jQuery);


In order not to be confused, we will call it a test plug-in, and the entire Sortable - the original one. The test plugin weighs exactly 1 KB, compressed 576 bytes, gzip 391 bytes.
')
/*! <br> * <br> * , 576 <br> */ <br>( function (d){ var _mouseCapture= function (a,b){ if ( this .reverting) return false ; if ( this .options.disabled|| this .options.type== "static" ) return false ; this ._refreshItems(a); var c= null ,e= this ;d(a.target).parents().each( function (){ if (d.data( this , "sortable-item" )==e){c=d( this ); return false }}); if (d.data(a.target, "sortable-item" )==e)c=d(a.target); if (!c) return false ; if ( this .options.handle&&!b){ var f= false ;d( this .options.handle,c).find( "*" ).andSelf().each( function (){ if ( this ==a.target)f= true }); if (!f) return false } this .currentItem=c; this ._removeCurrentsFromItems(); return true }})(jQuery);


this self


First, let's get rid of the omnipresent and incompressible expression of this . In our initial plugin, it is used as many as 587 times (which is 2.3 KB or 6% of the code). Add a local variable self to the beginning of the function:
var <br> self = this ;<br>

Now we replace everywhere in the body of this function with self and look at the results of compression.
It was:
this .currentItem=e; this ._removeCurrentsFromItems();

It became:
d.currentItem=e;d._removeCurrentsFromItems();

All test plugin:
/*! <br> * this > self <br> * YUI Compressor, 552 <br> */ <br>( function (b){ var a= function (f,g){ var d= this ; if (d.reverting){ return false } if (d.options.disabled||d.options.type== "static" ){ return false }d._refreshItems(f); var e= null ,c=b(f.target).parents().each( function (){ if (b.data( this , "sortable-item" )==d){e=b( this ); return false }}); if (b.data(f.target, "sortable-item" )==d){e=b(f.target)} if (!e){ return false } if (d.options.handle&&!g){ var h= false ;b(d.options.handle,e).find( "*" ).andSelf().each( function (){ if ( this ==f.target){h= true }}); if (!h){ return false }}d.currentItem=e;d._removeCurrentsFromItems(); return true }})(jQuery);

Not bad! By the way, in this method in line 19 there was already exactly the same declaration self = this to transfer the context to the function being created, but for some reason it was not used for optimization. Here we just brought this announcement to the beginning, and in another function one could add, the extra few characters will pay off if at least 4 this used in the function (and there are 70 of them).

Keys and strings


Now look carefully at the remaining code. Repeated expressions of options , target , data , "sortable-item" and others are clearly visible. Most of them are object property names or strings, some occur 50–60 times in the original plugin. Compressors compress these elements (change names to shorter ones) only Closure Compiler is able in advanced mode, but here we see almost all the properties in their places. Most likely, when compiling the library, an extensive list of externals, incompressible names was given. There is a natural desire to correct this situation, because the method of optimization suggests the very syntax of the language. Let's make frequently used keys local variables:
var <br> a = {<br> options: {<br> visible: true ,<br> mess: 'hi' <br> }<br> };

one:
if (a.options.visible) {<br> alert(a.options.mess);<br> a.options.visible = false ;<br>}

2:
var <br> o = 'options' ,<br> v = 'visible' ;<br> if (a[o][v]) {<br> alert(a[o].mess);<br> a[o][v] = false ;<br>}

In the second version, we needed to write visible only once instead of two, and options once instead of three. On the scale of a 40-kilobyte plug-in, and especially of the application, this gives a tangible gain in the amount of code (but not in clarity).

You can write a small script in any language to determine the frequency of expressions in the code and automatically replace them with variables. For example, in the following regular expression: /((\')|(\")|\.)\b([a-z_][\w-]+\w)\b(?(2)\')(?(3)\")/i . For the original plug-in, 175 replacements were obtained, for the test one - 15:
  1. / *!
  2. * Replacing this> self
  3. * Replacing keys and strings
  4. * /
  5. ( function ($) {
  6. var
  7. _reverting = 'reverting' , // 3
  8. _options = 'options' , // 51
  9. _disabled = 'disabled' , // 4
  10. _type = 'type' , // 2
  11. _static = 'static' , // 2
  12. __refreshItems = '_refreshItems' , // 2
  13. _target = 'target' , // 4
  14. _parents = 'parents' , // 2
  15. _each = 'each' , // 5
  16. _data = 'data' , // 5
  17. _sortable_item = 'sortable-item' , // 6
  18. _handle = 'handle' , // 2
  19. _find = 'find' , // 2
  20. _currentItem = 'currentItem' , // 52
  21. FALSE =! 1, // 30
  22. TRUE =! 0, // 11
  23. _mouseCapture = function ( event , overrideHandle) {
  24. var
  25. self = this ;
  26. if (self [_reverting]) {
  27. return FALSE;
  28. }
  29. if (self [_options] [_ disabled] || self [_options] [_ type] == _static) return FALSE;
  30. self [__ refreshItems] ( event );
  31. var currentItem = null , nodes = $ ( event [_target]) [_ parents] () [_ each] ( function () {
  32. if ($ [_ data] ( this , _sortable_item) == self) {
  33. currentItem = $ ( this );
  34. return FALSE;
  35. }
  36. });
  37. if ($ [_ data] ( event [_target], _sortable_item) == self) currentItem = $ ( event [_target]);
  38. if (! currentItem) return FALSE;
  39. if (self [_options] [_ handle] &&! overrideHandle) {
  40. var validHandle = FALSE;
  41. $ (self [_options] [_ handle], currentItem) [_ find] ( "*" ) .andSelf () [_ each] ( function () { if ( this == event [_target]) validHandle = TRUE;});
  42. if (! validHandle) return FALSE;
  43. }
  44. self [_currentItem] = currentItem;
  45. self._removeCurrentsFromItems ();
  46. return TRUE;
  47. };
  48. }) (jQuery);


Declaring variables is cumbersome, but you should not forget that it will be common for at least all methods of the original plug-in, in theory you can make a general declaration of names for the entire application. The larger the “field of activity”, the more effective the name compression will work, since each of them is indicated only once at the beginning of the module, and is subsequently replaced everywhere by its one- or two-letter representation. Compressed test plugin looks great:
  1. / *!
  2. * Replacing this> self
  3. * Replacing keys and strings
  4. * YUI Compressor, 395 B + JS beautifier
  5. * /
  6. ( function (c) {
  7. var b = function (v, w) {
  8. var t = this ;
  9. if (t [e]) {
  10. return o
  11. }
  12. if (t [m] [k] || t [m] [r] == f) {
  13. return o
  14. }
  15. t [n] (v);
  16. var u = null ,
  17. s = c (v [i]) [g] () [q] ( function () {
  18. if (c [l] ( this , j) == t) {
  19. u = c ( this );
  20. return o
  21. }
  22. });
  23. if (c [l] (v [i], j) == t) {
  24. u = c (v [i])
  25. }
  26. if (! u) {
  27. return o
  28. }
  29. if (t [m] [d] &&! w) {
  30. var x = o;
  31. c (t [m] [d], u) [a] ( "*" ) .andSelf () [q] ( function () {
  32. if ( this == v [i]) {
  33. x = h
  34. }
  35. });
  36. if (! x) {
  37. return o
  38. }
  39. }
  40. t [p] = u;
  41. t._removeCurrentsFromItems ();
  42. return h
  43. }
  44. }) (jQuery);


document , window , etc.


These global objects are also compressed well if they are assigned to local variables. In our test plug-in, document and window not used, so we _mouseDrag() temporarily turn to the neighboring _mouseDrag() method:
( function () {<br><br> if ( event .pageY - $( document ).scrollTop() < o.scrollSensitivity)<br> scrolled = $( document ).scrollTop($( document ).scrollTop() - o.scrollSpeed);<br> else if ($(window).height() - ( event .pageY - $( document ).scrollTop()) < o.scrollSensitivity)<br> scrolled = $( document ).scrollTop($( document ).scrollTop() + o.scrollSpeed);<br> <br> if ( event .pageX - $( document ).scrollLeft() < o.scrollSensitivity)<br> scrolled = $( document ).scrollLeft($( document ).scrollLeft() - o.scrollSpeed);<br> else if ($(window).width() - ( event .pageX - $( document ).scrollLeft()) < o.scrollSensitivity)<br> scrolled = $( document ).scrollLeft($( document ).scrollLeft() + o.scrollSpeed);<br><br>})();

Such a picture is quite often observed in scripts. Such code is compressed only due to the removal of spaces and unnecessary characters, since there are no local variables in it. Now let's look at compression with replacements:
( function () {<br> var g = document ,<br> f = window,<br> e = "scrollTop" ,<br> c = "scrollLeft" ,<br> b = "scrollSpeed" ,<br> d = "scrollSensitivity" ,<br> h = "pageY" ,<br> a = "pageX" ;<br><br> if ( event [h] - $(g)[e]() < o[d]) {<br> scrolled = $(g)[e]($(g)[e]() - o[b])<br> } else {<br> if ($(f).height() - ( event [h] - $(g)[e]()) < o[d]) {<br> scrolled = $(g)[e]($(g)[e]() + o[b])<br> }<br> }<br><br> if ( event [a] - $(g)[c]() < o[d]) {<br> scrolled = $(g)[c]($(g)[c]() - o[b])<br> } else {<br> if ($(f).width() - ( event [a] - $(g)[c]()) < o[d]) {<br> scrolled = $(g)[c]($(g)[c]() + o[b])<br> }<br> }<br><br>})();

This is the ABC of the optimizer, but for some reason these simple techniques are used extremely rarely, even in large libraries, where they belong. It is possible that if the developers came to certain agreements on the use of reduced variables at the project level, compressed jQuery could weigh not 70 KB, but, say, 50.

In the previous example, it would be even better to create a variable with reference to the document object in a jQuery wrapper:
var <br> doc = document ,<br> $doc = $(doc);


Lyrical digression

Quite often (yes almost always) in a dynamically created function in jQuery, an element in a wrapper is required, and you have to do something like this:
$( 'li' ).each( function (index, item) {<br> var <br> $item = $(item)<br> /* do something with $item */ <br>});

Such constructions are also found in the source code of the library. And how much simpler it would be if a jQuery object with an element were immediately passed to the third optional parameter:
$( 'li' ).each( function (index, item, $item) {<br> /* do something with $item */ <br>});

Beauty!

results


However, back to our test plugin. After optimization, it began to weigh 395 bytes versus 576, we won 31.4%:
/*! <br> * this > self <br> * <br> * YUI Compressor, 395 <br> */ <br>( function (c){ var b= function (v,w){ var t= this ; if (t[e]){ return o} if (t[m][k]||t[m][r]==f){ return o}t[n](v); var u= null ,s=c(v[i])[g]()[q]( function (){ if (c[l]( this ,j)==t){u=c( this ); return o}}); if (c[l](v[i],j)==t){u=c(v[i])} if (!u){ return o} if (t[m][d]&&!w){ var x=o;c(t[m][d],u)[a]( "*" ).andSelf()[q]( function (){ if ( this ==v[i]){x=h}}); if (!x){ return o}}t[p]=u;t._removeCurrentsFromItems(); return h}})(jQuery);


On the scale of the original plug-in, the picture, of course, is somewhat different, since variable declarations are added. As a result of all the manipulations, the plug-in compressed form began to weigh 18 KB exactly, the gain amounted to 5.3 KB (21.5%). But gzip, on the contrary, got 0.7 kb (13%), so it’s better to use this technique where gzp is unavailable for some reason. A file compressed this way can also be given to older browsers like Safari 2, which do not support gzip.

Source plugin size in bytes:
Source codethis → selfProperty caching
Without compression39,49540,24141,148
YUI / CC23,65622 18518,496
gzip5 8515,9506,504

Upd: added an example of compression of global objects.

Source: https://habr.com/ru/post/100828/


All Articles