Javascript.RU

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

Update: Более новый материал по этой теме находится по адресу https://learn.javascript.ru/better-minification.

При сжатии 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 нельзя отключить убивание переводов строки, поэтому получилось такое:

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 замечательно сжимает все, что только может.

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:

// Было
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, чтобы код понятнее был.

Внутри оператора 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();

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


Автор: sores (не зарегистрирован), дата: 20 июня, 2008 - 11:53
#permalink

ВОопщето вот лучший минимизатор!
http://dean.edwards.name/packer/


Автор: Илья Кантор, дата: 20 июня, 2008 - 12:36
#permalink

Это неправда.

packer не следует вообще использовать, если на сервере стоит mod_gzip/deflate/compress.


Автор: sunnybear (не зарегистрирован), дата: 23 июля, 2008 - 21:36
#permalink

Однозначно сказать нельзя. В общем случае, да. Но не всегда


Автор: Илья Кантор, дата: 23 июля, 2008 - 23:36
#permalink

Приведите, пожалуйста, хоть один пример скрипта, на котором результат packer + gzip весит меньше, чем просто gzip.


Автор: Гость (не зарегистрирован), дата: 20 июня, 2008 - 14:17
#permalink

В Опере страничка отображается неправильно


Автор: Илья Кантор, дата: 20 июня, 2008 - 23:43
#permalink

Да, совсем забыл об этом

Оказались непротестаны изменения верстки в опере. Поправил.


Автор: sunnybear (не зарегистрирован), дата: 23 июля, 2008 - 21:37
#permalink

К сожалению, все эти фишки актуальны только при отсутствии сжатия. Само сжатия уменьшает код раз в 5, поэтому дальнейшая оптимизация зачастую бессмысленна.


Автор: Илья Кантор, дата: 23 июля, 2008 - 23:36
#permalink

Зачастую сжатие в 5 раз еще не означает сжатие до нуля


Автор: Zebooka (не зарегистрирован), дата: 29 сентября, 2008 - 11:00
#permalink

Собственно не верное утверждение.
Ибо эти фишки можно использовать как обфускатор. Для запутывания кода.


Автор: миркус (не зарегистрирован), дата: 26 января, 2009 - 16:41
#permalink

а после тебя трынь трава?
другой разработчик будет сидеть голову ломать?

имхо,писать лучше читабельным кодом.
с комментами.

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


Автор: imsha, дата: 7 декабря, 2011 - 10:53
#permalink

Коммент неудачно написал уровнем ниже....

А сам через месяц когда вернешься поправить баг и сразу в ступор.
Мы на работе договорились даже об определенной семантике названия объектов и т.п. Пусть даже они будут длинными. Зато поддерживать удобнее. При факте - что вероятнее всего другой разработчик будет этим заниматься.

ТС, за статью спасибо. Буквально на днях скрещивал Tortoise Svn с батниками и YUI Compressor. Теперь есть о чем подумать и как все это дело допилить еще лучше.


Автор: Dima (не зарегистрирован), дата: 28 октября, 2008 - 22:32
#permalink

Для конкретного кода подходят разные типы сжатия кода.
Вообще, понравилась статья, узнал кое-что новое.


Автор: Мариана (не зарегистрирован), дата: 13 ноября, 2008 - 15:55
#permalink

Зачастую сжатие в 5 раз еще не означает сжатие до нуля


Автор: Нина (не зарегистрирован), дата: 22 ноября, 2008 - 13:33
#permalink

а для чего вообще сжимать, зачем такие сложности?


Автор: Илья Кантор, дата: 22 ноября, 2008 - 20:21
#permalink

Чтобы быстрее загружалось


Автор: gps (не зарегистрирован), дата: 13 августа, 2009 - 16:42
#permalink

Спасиб отличный сайт...


Автор: Гость (не зарегистрирован), дата: 3 сентября, 2009 - 13:51
#permalink

Отличный пакер!
Сделал простую обертку для запуска этого crunchy из командной строки:

<package>
<job>
<script language="javascript">
window = {};
var WS = WScript, oFSO = new ActiveXObject("Scripting.FileSystemObject"),
function alert(arg){
    WS.echo(arg)
};
function readFile(fileName) {
    var stream = oFSO.OpenTextFile(fileName),
        text = stream.ReadAll();
    stream.Close()
    return text
};

function createAndWriteFile(fileName, text) {
    var streem = oFSO.CreateTextFile(fileName);
    streem.Write(text);
};

function pack(sourceText) {
    sourceText = sourceText.replace(/\r*\n/g, "\n").replace(/\r/g, "\n");
    try {
        return Crunchy.crunch(sourceText);
    } catch (error) {

    }
};

</script>
<script src="web-crunchy.js" language="javascript"></script>
<script language="javascript">

var args = WS.arguments;
if( args.length < 1 ) WS.Quit(1);

var inputFileName = args.item(0);
var outputFileName = args.length < 2 ?
    inputFileName.replace(/(\.js$)|$/, "(crunchy-compressed).js") :
    args.item(1);

createAndWriteFile(outputFileName, pack(readFile(inputFileName)));

</script>
</job>
</package>

 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние темы на форуме
Forum