Using YUI Compressor with eval

Published 16 June 09 08:16 AM | jacobl 

YUI Compressor is an excellent JavaScript minifier and obfuscator. But every good thing comes with its compromises. While this tool prides itself on getting some of the best minification numbers I've seen, it's also very reliable. Your code gets smaller but will almost definitely work the same. While a few tools out there find trouble with eval and with, YUI Compressor gets around it by skipping it completely. It refuses to touch any code that is within the scope of an eval or with call. That might produce highly reliable, minified code, but in some cases, the code will just be reliable... unminified code. Good but not great.

Basic Case

Below is the example we'll be working with. We'll use it to demonstrate how eval can get trapped inside a scope causing YUI Compressor to ignore it completely.

   1: var Test = new function() {
   2:     var _self = this;
   3:     
   4:     function performOperation(scr) {
   5:         return "Result: " + eval(scr);
   6:     }
   7:  
   8:     _self.Operate = function(x, y, operation) {
   9:         return performOperation(x + operation + y);
  10:     }
  11: }

Above, the eval can reference anything inside the test class. It's hard to see how YUI could screw this up, but if we added an internal variable and modified the performOperation method to make use of it, you could see that things might get sticky.

   1: var Test = new function() {
   2:     var _self = this;
   3:     var _internalVariable = 3;
   4:  
   5:     function performOperation(scr) {
   6:         return "Result: " + eval("(" + scr + ") * _internalVariable");
   7:     }
   8:  
   9:     _self.Operate = function(x, y, operation) {
  10:         return performOperation(x + operation + y);
  11:     }
  12: }

This makes it a little more obvious. If YUI Compressor had it's way with this and wasn't as cautious as it is, _internalVariable would be renamed to some arbitrary string of letters and the eval call would fail.

You can see this by running the compressor on our script. The following code is the result:

   1: var Test=new function(){var _self=this;var _internalVariable=3;
   2: function performOperation(src){return"Result: "+eval("("+src+") * _internalVariable")}
   3: _self.Operate=function(x,y,operation){return performOperation(x+operation+y)}};

Nothing changed (return lines added for readability).

By adding the requirement that eval can only be called on public facing methods, we are then able to pull it out of the Test object entirely.

   1: var Test = {
   2:     Eval: function(src) {
   3:         return eval(src);
   4:     }
   5: }
   6: Test.Operator = new function() {
   7:     var _self = this;
   8:  
   9:     function performOperation(src) {
  10:         return "Result: " + Test.Eval(src);
  11:     }
  12:  
  13:     _self.Operate = function(x, y, operation) {
  14:         return performOperation(x + operation + y);
  15:     }
  16: }

While this allows for much more minified JavaScript, it does limit us a bit much. We can remedy this by adding a scope variable to the Test.Eval signature, allowing us to pass in the appropriate scope.

   1: var Test = {
   2:     Eval: function(src, scope) {
   3:         return eval(src);
   4:     }
   5: }
   6: Test.Operator = new function() {
   7:     var _self = this;
   8:     _self.Variable = 3;
   9:     
  10:     function performOperation(src) {
  11:         return "Result: " + Test.Eval("(" + src + ") * scope.Variable", _self);
  12:     }
  13:  
  14:     _self.Operate = function(x, y, operation) {
  15:         return performOperation(x + operation + y);
  16:     }
  17: }
  18:  

It might look a little confusing, but read it over; it's right.

Since YUI Compressor will not touch any code that has eval in its scope, we don't have to worry that the scope property will change.

   1: var Test={Eval:function(src,scope){return eval(src)}};
   2: Test.Operator=new function(){var a=this;a.Variable=3;
   3: function b(c){return"Result: "+Test.Eval("("+c+") * scope.Variable",a)}
   4: a.Operate=function(c,e,d){return b(c+d+e)}};

The only bit of code that is not minified is the Test.Eval method due to its use of eval. (return lines added for readability)

While this might not show much benefit as code-length savings goes, keep in mind that the example above is very small. Any reasonably long class will show huge improvements when minified properly; not to mention the benefits that are had from making your intellectual property less readable.

Real World Example

I will detail the following code in a future post, but for now, I wanted to show how the above technique of extracting eval calls out of a class can benefit JavaScript length after compression.

   1: var IAT = {};
   2: IAT.Eval = function(scr, scope) {
   3:     return eval(scr);
   4: }
   5: IAT.Importer = new function() {
   6:     var _self = this;
   7:     var _waitingClasses = [];
   8:     
   9:     function processWaitingClasses() {
  10:         for(var i = 0; i < _waitingClasses.length; i++) {
  11:             var refs = _waitingClasses[i].refs;
  12:             
  13:             var removeRefs = [];
  14:             for(var k = 0; k < refs.length; k++) {
  15:                 if(isDefined(refs[k]))
  16:                     removeRefs.push(k);
  17:             }
  18:             
  19:             for(var k = removeRefs.length - 1; k >= 0; k--) {
  20:                 refs.splice(removeRefs[k], 1);
  21:             }
  22:                 
  23:             if(_waitingClasses[i].refs.length < 1) {
  24:                 var cls = _waitingClasses[i];
  25:                 _waitingClasses.splice(i, 1);
  26:                 setClass(cls);
  27:                 break;
  28:             }
  29:         }
  30:     }
  31:     
  32:     function setClass(def) {
  33:         // 'scope' will be evaluated as def in IAT.Eval
  34:         IAT.Eval(def.clsName + '=scope.cls();', def);
  35:         processWaitingClasses();
  36:     }
  37:     
  38:     function isDefined(str, scope) {
  39:         if(!scope) scope = 'window';
  40:         var path = str.split('.');
  41:         
  42:         var scr = scope + '.' + path[0];
  43:         if(!IAT.Eval(scr)) return false;
  44:         
  45:         if(path.length == 1) return true;
  46:         
  47:         path.splice(0,1);
  48:         
  49:         str = path.join('.');
  50:         if(path.length > 1) str = str.substring(0, str.length);
  51:         
  52:         return isDefined(str, scr);
  53:     }
  54:     
  55:     _self.Ns = function(str, scope) {
  56:         if(!scope) scope = 'window';
  57:         var path = str.split('.');
  58:         
  59:         var scr = scope + '.' + path[0];
  60:         if(!IAT.Eval(scr)) IAT.Eval(scr + '={};');
  61:         
  62:         if(path.length == 1) return;
  63:         
  64:         path.splice(0,1);
  65:         
  66:         str = path.join('.');
  67:         if(path.length > 1) str = str.substring(0, str.length);
  68:         
  69:         this.Ns(str, scr);
  70:     }
  71:     
  72:     _self.DefineClass = function(config) {
  73:         var parent = config.clsName.substring(0, config.clsName.lastIndexOf('.'));
  74:         _self.Ns(parent);
  75:         _waitingClasses.push(config);
  76:         for(var i = 0; i < config.refs.length; i++) {
  77:             _self.Import(config.refs[i]);
  78:         }
  79:         processWaitingClasses();
  80:     }
  81:     
  82:     _self.Import = function(clsName) {
  83:         if(isDefined(clsName)) return;
  84:     
  85:         var path = clsName.split('.');
  86:         path.splice(0,1);
  87:         var clsPath = path.join('/');
  88:         clsPath = clsPath.substring(0, clsPath.length);
  89:         
  90:         var head = document.getElementsByTagName('head')[0];
  91:         var script = document.createElement('script');
  92:         script.setAttribute('type','text/javascript');
  93:         script.setAttribute('language','javascript');
  94:         script.setAttribute('src','Library/js/' + clsPath + '.js');
  95:         head.appendChild(script);
  96:     }
  97: }
  98:  

(Before YUI Compressor)

   1: var IAT={};IAT.Eval=function(scr,scope){return eval(scr)};
   2: IAT.Importer=new function(){var a=this;var d=[];
   3: function c(){for(var j=0;j<d.length;j++){
   4: var h=d[j].refs;var l=[];for(var g=0;g<h.length;g++){
   5: if(b(h[g])){l.push(g)}}for(var g=l.length-1;g>=0;g--){
   6: h.splice(l[g],1)}if(d[j].refs.length<1){
   7: var f=d[j];d.splice(j,1);e(f);break}}}
   8: function e(f){IAT.Eval(f.clsName+"=scope.cls();",f);c()}
   9: function b(i,f){if(!f){f="window"}var h=i.split(".");
  10: var g=f+"."+h[0];if(!IAT.Eval(g)){return false}
  11: if(h.length==1){return true}h.splice(0,1);i=h.join(".");
  12: if(h.length>1){i=i.substring(0,i.length)}return b(i,g)}
  13: a.Ns=function(i,f){if(!f){f="window"}var h=i.split(".");
  14: var g=f+"."+h[0];if(!IAT.Eval(g)){IAT.Eval(g+"={};")}
  15: if(h.length==1){return}h.splice(0,1);i=h.join(".");
  16: if(h.length>1){i=i.substring(0,i.length)}this.Ns(i,g)};
  17: a.DefineClass=function(f){
  18: var h=f.clsName.substring(0,f.clsName.lastIndexOf("."));
  19: a.Ns(h);d.push(f);for(var g=0;g<f.refs.length;g++){
  20: a.Import(f.refs[g])}c()};a.Import=function(i){
  21: if(b(i)){return}var j=i.split(".");
  22: j.splice(0,1);var f=j.join("/");
  23: f=f.substring(0,f.length);var h=document.getElementsByTagName("head")[0];
  24: var g=document.createElement("script");
  25: g.setAttribute("type","text/javascript");
  26: g.setAttribute("language","javascript");
  27: g.setAttribute("src","Library/js/"+f+".js");h.appendChild(g)}};
  28:  

(After YUI Compressor - Return lines added for readability - Over a 25% savings!)

The character length drops from 1788 before compression to 1317 after. Any additional code that is added to this class not containing eval will only increase the savings percentage; making your users' downloading experience that much snappier!

Filed under: , ,

Comments

No Comments
Anonymous comments are disabled