Улучшаем сжимаемость Javascript-кода.

При сжатии javascript-кода минификатор делает две основные вещи.

  1. удаляет заведомо лишние символы: пробелы, комментарии и т.п.
  2. заменяет локальные переменные более короткими.

В статье рассматриваются минификаторы YUI Compressor и ShrinkSafe.
На момент написания это лучшие минификаторы javascript.

Есть несколько несложных приемов программирования, которые могут увеличить сжимаемость JS-кода.

Минификация переменных

Минификатор заменяет все локальные переменные на более короткие
(Также о сжатии в статье Сжатие Javascript и CSS).

Например, вот такой скрипт:

function flyToMoon(moon) {
  var spaceShip = new SpaceShip()
  spaceShip.fly(moon.getDistance())
}
</div>

После минификации станет:

function flyToMoon(A) {
  var B = new SpaceShip()
  B.fly(A.getDistance())
}

Заведомо локальные переменные moon, spaceShip могут быть безопасно заменены на более короткие A,B. Минификатор использует патченный открытый интерпретатор javascript, написанный на java: Rhino, он разбирает код, анализирует и собирает обратно с заменой, поэтому все делается корректно.

С другой стороны, название функции в этом скрипте - глобальная переменная, оно не сжимается, чтобы сохранить возможность обращения к функции.

По тем же причинам не сжимается вызов SpaceShip и методы fly, getDistance().

Итак, сделаем какой-нибудь более реальный скрипт и напустим на него YUI Compressor.

Тестовый скрипт

function SpaceShip(fuel) {
    
    this.fuel = fuel
    
    this.inflight = false
        
    this.showWarning = function(message) {
        var warningBox = document.createElement('div')
        with (warningBox) {
            innerHTML = message
            className = 'warningBox'
        }
        document.body.appendChild(warningBox)        
    }
    
    this.showInfo = function(message) {
        var messageBox = document.createElement('div')
        messageBox.innerHTML = message
        document.body.appendChild(messageBox)        
    }
    
    this.fly = function(distance) {
        if (distance<this.fuel) {
            this.showWarning("Мало топлива!")
        } else {            
            this.inflight = true
            this.showInfo("Взлетаем")
        }
    }
    
}

function flyToMoon() {
    var spaceShip = new SpaceShip()
    spaceShip.fly(1000)
}

flyToMoon()

YUI Compressor

К сожалению, в YUI Compressor нельзя отключить убивание переводов строки, поэтому получилось такое:

function SpaceShip(fuel){this.fuel=fuel;
this.inflight=false;this.showWarning=function(message){
var warningBox=document.createElement("div");
with(warningBox){innerHTML=message;
className="warningBox"
}document.body.appendChild(warningBox)
};this.showInfo=function(message){
var messageBox=document.createElement("div");
messageBox.innerHTML=message;
document.body.appendChild(messageBox)
};this.fly=function(distance){
if(distance<this.fuel){this.showWarning("Мало топлива!")
}else{this.inflight=true;
this.showInfo("Взлетаем")
}}}function flyToMoon(){var A=new SpaceShip();
A.fly(1000)};flyToMoon()

Если посмотреть внимательно - видно, что локальные переменные вообще не сжались внутри SpaceShip, а сжались только в функции flyToMoon.

ShrinkSafe

ShrinkSafe замечательно сжимает все, что только может.

function SpaceShip(_1){
this.fuel=_1;
this.inflight=false;
this.showWarning=function(_3){
var _4=document.createElement("div");
with(_4){
innerHTML=_3
className="warningBox";
}
document.body.appendChild(_4);
};
this.showInfo=function(_5){
var _6=document.createElement("div");
_6.innerHTML=_5;
document.body.appendChild(_6);
};
this.fly=function(_7){
if(_7<this.fuel){
this.showWarning("Мало топлива!");
}else{
this.inflight=true;
this.showInfo("Взлетаем");
}
};
};
function flyToMoon(){
var _8=new SpaceShip();
_8.fly(1000);
};
flyToMoon();

Все локальные переменные заменены на более короткие _варианты.

Почему YUI не сжал, а ShrinkSafe справился?

Дело в конструкции with() { ... } в методе showWarning. Эти два компрессора по-разному к ней относятся.

ShrinkSafe + with

ShrinkSafe игнорирует нелокальные названия переменных внутри with:

// Было
with(val) {
  prop = val
}
// Стало
with(_1) {
  prop = _1
}

К сожалению, в последней версии ShrinkSafe есть баг: если переменная prop объявлена локально, то она заменяется:

// Было
var prop
with(val) {
  prop = val
}
// Стало
var _1
with(_2) {
  _1 = _2
}

Например, если val = { prop : 5 }, то сжатая таким образом конструкция with сработает неверно из-за сжатия prop до _1.

Впрочем, никогда не слышал, чтобы кто-то на такой баг реально напоролся. Видимо, люди локальные переменные не объявляют одноименные со свойствами аргумента with, чтобы код понятнее был.

YUI Compressor + with

Внутри оператора with(obj) никогда нельзя точно сказать: будет ли переменная взята из obj или из внешней области видимости.

Поэтому никакие переменные внутри with сжимать нельзя.

А раз так - получается, что локальные переменные с именами, упомянутыми внутри with тоже сжимать нельзя.

YUI Compressor почему-то (почему? есть идеи?) пошел еще дальше: он не минифицирует вообще никаких локальных переменных даже в соседних функциях.

Может быть, это баг (версия 2.3.5), а может - фича, не знаю. Будут идеи - пишите в комментариях. Например, локальные переменные функции fly вполне можно было минифицировать.

Вывод:

YUI категорически не любит with.

ShrinkSafe любит, но с багофичей.

Если заменить функцию showWarning на вариант без with, то YUI сожмет код без проблем:

// вариант без with
this.showWarning = function(message) {
        var warningBox = document.createElement('div')
        warningBox.innerHTML = message
        warningBox.className = 'warningBox'        
        document.body.appendChild(warningBox)        
}

Результат сжатия YUI без with:

function SpaceShip(A){this.fuel=A;
this.inflight=false;this.showWarning=function(B){var C=document.createElement("div");
C.innerHTML=B;C.className="warningBox";
document.body.appendChild(C)
};this.showInfo=function(B){var C=document.createElement("div");
C.innerHTML=B;document.body.appendChild(C)
};this.fly=function(B){if(B<this.fuel){this.showWarning("Мало топлива!")
}else{this.inflight=true;
this.showInfo("Взлетаем")
}}}function flyToMoon(){var A=new SpaceShip();
A.fly(1000)}

Обращения ко внешним переменным

В примере не сжались вызовы к объекту document.

Для того, чтобы сжатие сработало, надо заменить обращение к глобальной переменной document вызовом локальной функции.

Например, вот так:

function SpaceShip(fuel) {
    /* сокращенные локальные вызовы */
    var doc = document
    var createElement = function(str) {
        return doc.createElement(str)
    }
    var appendChild = function(elem) {
        doc.body.appendChild(elem)
    }

    this.fuel = fuel
    
    this.inflight = false
        
    this.showWarning = function(message) {
        var warningBox = createElement('div')
        warningBox.innerHTML = message
        warningBox.className = 'warningBox'        
        appendChild(warningBox)        
    }
    
    this.showInfo = function(message) {
        var messageBox = createElement('div')
        messageBox.innerHTML = message
        appendChild(messageBox)        
    }
    
    this.fly = function(distance) {
        if (distance<this.fuel) {
            this.showWarning("Мало топлива!")
        } else {            
            this.inflight = true
            this.showInfo("Взлетаем")
        }
    }    
}

Обращение к document осталось в одном месте, что тут же улучшает сжатие:

(Здесь и дальше для сжатия использован ShrinkSafe, т.к он оставляет переводы строки.
Результаты YUI - по сути, такие же)

function SpaceShip(_1){
var _2=document;
var _3=function(_4){
return _2.createElement(_4);
};
var _5=function(_6){
_2.body.appendChild(_6);
};
this.fuel=_1;
this.inflight=false;
this.showWarning=function(_7){
var _8=_3("div");
_8.innerHTML=_7;
_8.className="warningBox";
_5(_8);
};
this.showInfo=function(_9){
var _a=_3("div");
_a.innerHTML=_9;
_5(_a);
};
this.fly=function(_b){
if(_b<this.fuel){
this.showWarning("Мало топлива!");
}else{
this.inflight=true;
this.showInfo("Взлетаем");
}
};

Как правило, в интерфейсах достаточно много обращений к document, и все они длинные, поэтому этот подход может уменьшить сразу код эдак на 10-20%.

Функции объявлены через var, а не function:

var createElement = function(str) { // (1)
  return doc.createElement(str)
}
// а не
function createElement(str) {  // (2)
  return doc.createElement(str)
}

Это нужно для ShrinkSafe, который сжимает только определения (1). Для YUI - без разницы, как объявлять функцию, сожмет и так и так.

Приватизация свойств объекта

Существуют различные способы объявления объектов.
Один из них - фабрика объектов, когда для создания не используется оператор new.

Общая схема фабрики объектов:

function object() {
  var private = 1  // приватная переменная для будущего объекта

  return {   // создаем объект прямым объявлением в виде { ... }
    increment: function(arg) {  // открытое свойство объекта
        arg += private // доступ к приватной переменной
        return arg
    }
  }
}
// вызов не new object(), а просто
var obj = object()

Прелесть тут состоит в том, что приватные переменные являются локальными, и поэтому могут быть сжаты. Кроме того, убираются лишние this.

function SpaceShip(fuel) {
    var doc = document

    var createElement = function(str) {
        return doc.createElement(str)
    }
    var appendChild = function(elem) {
        doc.body.appendChild(elem)
    }
    
    var inflight = false    
    
    var showWarning = function(message) {
        var warningBox = createElement('div')
        warningBox.innerHTML = message
        warningBox.className = 'warningBox'        
        appendChild(warningBox)        
    }
    
    var showInfo = function(message) {
        var messageBox = createElement('div')
        messageBox.innerHTML = message
        appendChild(messageBox)        
    }
    
    return {
        fly: function(distance) {
            if (distance<this.fuel) {
                showWarning("Мало топлива!")
            } else {            
                inflight = true
                showInfo("Взлетаем")
            }
        }
    }
    
}

function flyToMoon() {
    var spaceShip = SpaceShip()
    spaceShip.fly(1000)
}
function SpaceShip(_1){
var _2=document;
var _3=function(_4){
return _2.createElement(_4);
};
var _5=function(_6){
_2.body.appendChild(_6);
};
var _7=false;
var _8=function(_9){
var _a=_3("div");
_a.innerHTML=_9;
_a.className="warningBox";
_5(_a);
};
var _b=function(_c){
var _d=_3("div");
_d.innerHTML=_c;
_5(_d);
};
return {fly:function(_e){
if(_e<this.fuel){
_8("Мало топлива!");
}else{
_7=true;
_b("Взлетаем");
}
}};
};
function flyToMoon(){
var _f=SpaceShip();
_f.fly(1000);
};
flyToMoon();

Максимально возможное использование локальных переменных, собственно, и улучшает минификацию. А некоторые подходы в этой статье - лишь иллюстрации.