JavaScript

индекс
246,38

Сам себе gzip: сжимаем скрипты на 20% лучше

Повторяющиеся ключи

Если посмотреть на скрипт, сжатый Closure Compiler'ом, YUI Compressor'ом или еще чем-нибудь, можно увидеть бесконечные вереницы повторяющихся ключей: .prototype, .length, offsetParent и так далее. Попробуем избавиться от них на примере плагина jQuery UI Sortable. Скажу сразу, что gzip нам не переплюнуть, но когда его нет под рукой или нельзя им воспользоваться (например, на конкурсе 10K Apart), эта техника сжатия может оказаться весьма полезной.

Возьмем файл jquery.ui.sortable.js версии 1.8.2 как один из самых внушительных в комплекте jQuery UI. Исходник весит 38,5 КБ, сжатый Closure Compiler'ом — 23,1 КБ, в gzip'е — 5,71 КБ. Чтобы не загромождать экраны, выберем какой-нибудь характерный метод и представим, что это отдельный плагин. Мне понравился _mouseCapture():
  1. /*!
  2. * Исходный код
  3. */
  4. (function($) {
  5.  
  6. var
  7.     _mouseCapture = function(event, overrideHandle) {
  8.  
  9.         if (this.reverting) {
  10.             return false;
  11.         }
  12.  
  13.         if (this.options.disabled || this.options.type == 'static') return false;
  14.  
  15.         //We have to refresh the items data once first
  16.         this._refreshItems(event);
  17.  
  18.         //Find out if the clicked node (or one of its parents) is a actual item in this.items
  19.         var currentItem = null, self = this, nodes = $(event.target).parents().each(function() {
  20.             if ($.data(this, 'sortable-item') == self) {
  21.                 currentItem = $(this);
  22.                 return false;
  23.             }
  24.         });
  25.         if ($.data(event.target, 'sortable-item') == self) currentItem = $(event.target);
  26.  
  27.         if (!currentItem) return false;
  28.         if (this.options.handle && !overrideHandle) {
  29.             var validHandle = false;
  30.  
  31.             $(this.options.handle, currentItem).find("*").andSelf().each(function() { if (this == event.target) validHandle = true; });
  32.             if (!validHandle) return false;
  33.         }
  34.  
  35.         this.currentItem = currentItem;
  36.         this._removeCurrentsFromItems();
  37.         return true;
  38.  
  39.     };
  40.  
  41. })(jQuery);


Чтобы не путаться, будем называть его тестовым плагином, а весь Sortable — исходным. Тестовый плагин весит ровно 1 КБ, в сжатом виде 576 байт, в gzip 391 байт.

/*!
* Исходный код
* Оригинальное сжатие, 576 Б
*/
(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


Для начала избавимся от вездесущего и несжимающегося выражения this. В нашем исходном плагине оно употребляется ни много ни мало 587 раз (что составляет 2,3 КБ или 6% кода). Добавим в начало функции локальную переменную self:
var
    self = this;

Теперь заменим везде в теле функции this на self и посмотрим на результаты сжатия.
Было:
this.currentItem=e;this._removeCurrentsFromItems();

Стало:
d.currentItem=e;d._removeCurrentsFromItems();

Весь тестовый плагин:
/*!
* Замена this > self
* YUI Compressor, 552 Б
*/
(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);

Неплохо! Кстати, в этом методе в строке 19 уже было точно такое же объявление self = this для передачи контекста в создаваемую функцию, но почему-то не использовалось для оптимизаци. Здесь мы просто вынесли это объявление в начало, а в другой функции можно было бы и добавить, лишние несколько символов окупятся, если в функции используются хотя бы 4 this (а их бывает и по 70).

Ключи и строки


Теперь посмтрим внимательно на оставшийся код. Хорошо видны повторяющиеся выражения options, target, data, "sortable-item" и другие. Большая часть из них — имена свойств объектов или строки, некоторые встречаются в исходном плагине по 50—60 раз. Из компрессоров сжимать эти элементы (менять имена на более короткие) умеет только Closure Compiler в продвинутом режиме, но тут мы наблюдаем почти все свойства на своих местах. Скорее всего, при компиляции библиотеки был задан обширный список экстернов, несжимаемых имен. Возникает естественное желание иправить эту ситуацию, ведь способ оптимизации подсказывает сам синтаксис языка. Сделаем часто используемые ключи локальными переменными:
var
    a = {
        options: {
            visible: true,
            mess: 'hi'
        }
    };


1:
if (a.options.visible) {
    alert(a.options.mess);
    a.options.visible = false;
}


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

Во втором варианте нам понадобилось написать visible всего один раз вместо двух, а options один раз вместо трех. В маштабе 40-килобайтного плагина, а тем более всего приложения, это дает ощутимый выигрыш в размере кода (но не в понятности).

Можно написать небольшой скрипт на любом языке для определения частоты выражений в коде и автоматической замены их на переменные. Например, по такому регулярному выражению: /((\')|(\")|\.)\b([a-z_][\w-]+\w)\b(?(2)\')(?(3)\")/i. Для исходного плагина получилось 175 замен, для тестового — 15:
  1. /*!
  2. * Замена this > self
  3. * Замена ключей и строк
  4. */
  5. (function($) {
  6.  
  7. var
  8.     _reverting = 'reverting', // 3
  9.     _options = 'options', // 51
  10.     _disabled = 'disabled', // 4
  11.     _type = 'type', // 2
  12.     _static = 'static', // 2
  13.     __refreshItems = '_refreshItems', // 2
  14.     _target = 'target', // 4
  15.     _parents = 'parents', // 2
  16.     _each = 'each', // 5
  17.     _data = 'data', // 5
  18.     _sortable_item = 'sortable-item', // 6
  19.     _handle = 'handle', // 2
  20.     _find = 'find', // 2
  21.     _currentItem = 'currentItem', // 52
  22.     FALSE = !1, // 30
  23.     TRUE = !0, // 11
  24.  
  25.     _mouseCapture = function(event, overrideHandle) {
  26.         var
  27.             self = this;
  28.  
  29.         if (self[_reverting]) {
  30.             return FALSE;
  31.         }
  32.  
  33.         if(self[_options][_disabled] || self[_options][_type] == _static ) return FALSE;
  34.  
  35.  
  36.         self[__refreshItems](event);
  37.  
  38.  
  39.         var currentItem = null, nodes = $(event[_target])[_parents]()[_each](function() {
  40.             if($[_data](this, _sortable_item) == self) {
  41.                 currentItem = $(this);
  42.                 return FALSE;
  43.             }
  44.         });
  45.         if($[_data](event[_target], _sortable_item) == self) currentItem = $(event[_target]);
  46.  
  47.         if(!currentItem) return FALSE;
  48.         if(self[_options][_handle] && !overrideHandle) {
  49.             var validHandle = FALSE;
  50.  
  51.             $(self[_options][_handle], currentItem)[_find]("*").andSelf()[_each](function() { if(this == event[_target]) validHandle = TRUE; });
  52.             if(!validHandle) return FALSE;
  53.         }
  54.  
  55.         self[_currentItem] = currentItem;
  56.         self._removeCurrentsFromItems();
  57.         return TRUE;
  58.  
  59.     };
  60.  
  61. })(jQuery);


Объявление переменных выглядит громоздко, но не стоит забывать, что оно будет общим как минимум для всех методов исходного плагина, в теории же можно сделать общее объявление имен для всего приложения. Чем больше «поле деятельности», тем эффективнее будет работать сжатие имен, т.к. каждое из них указывается только один раз в начале модуля, и в дальнейшем везде заменяется своим одно- или двухбуквенным представлением. В сжатом виде тестовый плагин выглядит отлично:
  1. /*!
  2. * Замена this > self
  3. * Замена ключей и строк
  4. * YUI Compressor, 395 Б + 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.


Эти глобальные объекты тоже отлично сжимаются, если присвоить их локальным переменным. В нашем тестовом плагине document и window не используются, поэтому временно обратимся к соседнему методу _mouseDrag():
(function() {

    if(event.pageY - $(document).scrollTop() < o.scrollSensitivity)
        scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
    else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity)
        scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
    
    if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity)
        scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
    else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity)
        scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);

})();

Такая картина довольно часто наблюдается в скриптах. Сжимается такой код только за счет удаления пробелов и ненужных символов, так как в нем нет локальных переменных. А теперь посмотрим на сжатие с заменами:
(function () {
    var g = document,
        f = window,
        e = "scrollTop",
        c = "scrollLeft",
        b = "scrollSpeed",
        d = "scrollSensitivity",
        h = "pageY",
        a = "pageX";

    if (event[h] - $(g)[e]() < o[d]) {
        scrolled = $(g)[e]($(g)[e]() - o[b])
    } else {
        if ($(f).height() - (event[h] - $(g)[e]()) < o[d]) {
            scrolled = $(g)[e]($(g)[e]() + o[b])
        }
    }

    if (event[a] - $(g)[c]() < o[d]) {
        scrolled = $(g)[c]($(g)[c]() - o[b])
    } else {
        if ($(f).width() - (event[a] - $(g)[c]()) < o[d]) {
            scrolled = $(g)[c]($(g)[c]() + o[b])
        }
    }

})();

Это азбука оптимизатора, но почему-то эти простые приемы используются крайне редко, даже в больших библиотеках, где им самое место. Вполне возможно, что если бы разработчики пришли к определенным соглашениям по использованию сокращенных переменных на уровне проекта, сжатый jQuery мог бы весить не 70 КБ, а, допустим, 50.

В предыдущем примере еще лучше было бы создать переменную со ссылкой на объект документа в обертке jQuery:
var
    doc = document,
    $doc = $(doc);


Лирическое отступление

Довольно часто (да почти всегда) в динамически создаваемых функциях в jQuery требуется элемент в обертке, и приходится делать что-то такое:
$('li').each(function(index, item) {
    var
        $item = $(item)
    /* do something with $item */
});

Такие конструкции встречаются и в исходниках библиотеки. А насколько было бы проще, если бы jQuery-объект с элементом передавался бы сразу третьим необязательным параметром:
$('li').each(function(index, item, $item) {
    /* do something with $item */
});

Красота!

Результаты


Однако, вернемся к нашему тестовому плагину. После оптимизации он стал весить 395 байт против 576, мы выиграли 31,4%:
/*!
* Замена this > self
* Замена ключей и строк
* YUI Compressor, 395 Б
*/
(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);


В маштабе исходного плагина картина, разумеется, несколько иная, так как добавляются объявления переменных. В результате всех манипуляций плагин в сжатом виде стал весить 18 КБ ровно, выигрыш составил 5,3 КБ (21,5%). А вот gzip, наоборот, потяжелел на 0,7 КБ (13%), так что эту технику лучше использовать там, где gzp по каким-то причинам недоступен. Сжатый таким образом файл также можно отдавать старым браузерам вроде Safari 2, не поддерживающим gzip.

Размеры исходного плагина в байтах:
Исходник this → self Кэширование свойств
Без сжатия 39 495 40 241 41 148
YUI / CC 23 656 22 185 18 496
gzip 5 851 5 950 6 504

Upd: добавлен пример сжатия глобальных объектов.
+38
3 августа 2010, 09:08
67

комментарии (51)

–1
p0is0n #
Теперь заменим везде в теле функции this на self и посмотрим на результаты сжатия.


а на сколько t тяжелее s?
+2
AstralMan #
А вы пробовали сравнить два результата? Там все видно.
+11
Bambr #
Там в результатах используется переменная d, а не self, что в целом понятно.
–5
p0is0n #
Вы это серьезно сказали?
0
TheShock #
this нельзя сжимать. а вот self — можно
0
tenshi #
зато доступ к this быстрее
0
googol #
В Google Closure Compiler есть возможнось алиасить переменные this, window, document. Но от этого отказались по умолчанию потому как это уменьшает скорость выполнения ~10%. В особенности V8 не любит это.

Спроситите в maillist они вам более развернутый ответ дадут.
+9
Bambr #
Скажу сразу, что gzip нам не переплюнуть, но когда его нет под рукой, эта техника сжатия может оказаться весьма полезной.
Прям страшный сон какой-то, нет под рукой gzip :)
+2
bondbig #
опередил меня на секунды )
+9
realovich #
может автор имеет такой gzip… ) его-то может и не оказаться под рукой, тогда способ актуален:
+3
bondbig #
gzip нам не переплюнуть, но когда его нет под рукой
Это как, простите?
+1
Fesor #
GZip есть везде, другое дело что включать его иногда не хотят по каким-то причинам. Но в продакшен можно уже и включить.
0
bondbig #
так при чем тут ваш метод? Таки заставить сжиматься, независимо от того, включен гзип или нет? Плохая идея. То, что можно сделать на уровне сервера (и сделать хорошо), никак не стоит делать на уровне приложения.
0
Fesor #
Это не мой метод, поправочка маленькая ;)
0
bondbig #
а, ну да. )
0
altspam #
Бывает всякое: требования заказчика, старые браузеры, спортивный интерес… :) Кроме того, я думаю, можно организовать код так, чтобы он эффективнее сжимался gzip'ом, и тогда оптимизированные скрипты будут выигрывать и в .gz.
+1
homm #
Старые браузеры никак не мешают отдавать gzip новым.
0
altspam #
Разумеется.
+3
homm #
Как-то не хватает итогов, на сколько с применением данных техник стали меньше исходники, сжатые Closure Compiler и YUI Compressor, по сравнению с оригинальными исходниками, сжатыми ими же?
+1
altspam #
Каким-то необъяснимым образом часть поста осталась в черновиках :) Спасибо за замечание, читайте пост целиком.
НЛО прилетело и опубликовало эту надпись здесь
0
asm0dey #
GWT?
НЛО прилетело и опубликовало эту надпись здесь
0
asm0dey #
На самом деле я был не совсем прав.
Все-таки там генерируется джаваскрипт, который достаточно сильно сжат именно вербальными методами. Но мне затруднительно сказать, сколько он экономит трафика, потому что он еще и обфусцирован до полной невозможности восприятия.
А сам GWT — это возможность написать клиент-серверное веб-приложение на php или java без знания html, javascript и css
0
ad_Wolf #
А разве GWT существует не только под Java'у?
+1
asm0dey #
Родной он под джаву, но
code.google.com/p/gwtphp/
www.gwtphp.com/

Насколько они актуальны, я не знаю, потому что сам пишу на джаве.
0
chaliy #
Это тока сервеная часть. Клиентскую так и остается на java.
0
asm0dey #
/Клиентская часть — это по сути всего навсего один js+5 html файликов. Не думаю, что большой проблемой является их распаковать из war архива и заставить отдаваться через апач/инжиникс.
0
chaliy #
GWT это штука которая компилит java в javascript. Как одну из фитчей он потдерживает комуникацию с сервером. gwtphp это простейшая реализация протокола которым пользуется gwt. Никаких компиляций клиентского кода єта либа не потдерживает.
0
asm0dey #
А, то есть как бэкэнд нам все равно нужен сервер типа томката или глассфиша?
Для меня просто тема не особо актуальна, потому что у нас в продакшене все равно томкат используется.
0
chaliy #
То что компилит GWT для клиентской части, это набор статических файлов. JS, CSS, HTML и картинки. Их можно хостить где угодно. Мы например .NET стек, и хостим их под IIS. А вообще собираемся на CDN.

А уж серверная часть может быть быть начем угодно, по умолчанию это java и соотвевенно требует апп сервер. Но вполне может быть чем угодно. У нас это .NET. Вот и gwtphp, это тока серверная часть. Маленький опциональный кусочек.

Тоесть вы все ранво продолжаете писать клиентскую часть на java. Компилить ее в статические файлы. А сервер уже PHP.

Это я к тому что не существует GWT не под java. Есть интеграции с разными серверными стеками.
0
asm0dey #
А, теперь понял.
Да, мне как-то не пришло в голову, что именно джава компилится в джаваскрипт и так далее.
Прошу прощения за тупость.
Ну и в итоге имеем, что при желании GWT-приложение можно поднять на бесплатном хостинге с php+mysql. До вас мне эта мысль в голову не приходила, спасибо.
А нет ли аналогичных фреймворков для того же php? Чтобы мы написали на php, запустили, у нас создалось некоторое количкство файликов?
0
chaliy #
Про аналогичные фреймворки на PHP я не в курсе. Я из .NET стека. Под .NET есть, но они все пока-что уступают GWT.
0
iShaman #
Тестовый плагин весит ровно 1 КБ, в сжатом виде 576 Байт, в gzip 391 КБ.

391 килобайт?:)
0
altspam #
Ой, 391 байт конечно же :)
+4
erfen #
Тестовый плагин весит ровно 1 КБ, в сжатом виде 576 Байт, в gzip 391 КБ.
Если там действительно килобайты, то Вы очень неплохо переплюнули gzip :)
0
altspam #
:)
+1
build_your_web #
По-моему, правильное решение в этом случае — это написать предложение в саппорт Google Compiler'a и прочих сжимальщиков.

Код всегда должен оставаться как можно более читабельным, с возможностью быстрого сжатия специализированными тулзами.
+1
donnerjack13589 #
Есть одна проблема. Дело в том, что обращение к методу через переменную более долгое, чем напрямую по имени.
Т.е.

obj["method"](); // Дольше, чем:
obj.method();

Я раньше тоже использовал технику кэширования имен атрибутов и методов, но потом провел тесты и решил отказаться от этого в пользу производительности.
0
Aralot #
о, расскажите, пожалуйста, поподробнее? насколько дольше?
0
altspam #
Разумеется, этот подход не для тех приложений, в которых критична скорость (впрочем, как и ООП с его наследованием и другие навороты). Но если в пределах 5 строк 12 раз встречаются конструкции типа $(document), то уж обращение к свойству через переменную вы точно можете себе позволить.
0
asis #
ну тогда вы выбрали не удачную библиотеку для примеров. В ней как раз все критично.
0
sunnybear #
в таком варианте — все равно. Производительность обычно на вызовах функций падает, а не на разыменовании
0
sunnybear #
Packer тоже примерно также писали. Только вот инициализация такого сжатого скрипта — неблагодарное занятие.
+1
sunnybear #
также -> так же
0
iStyx #
Отличная статья, спасибо. Кстати, вы там случайно функцию копирайта выложили ;)
0
altspam #
Спасибо :) Поправил.
0
akirsanov #
Можно было бы завернуть весь скрипт в строку, провести часточный анализ слов и заменять самые используемые слова в строке на неиспользуемые байты.
Распаковщик будет в 50 байт.
0
lol2Fast4U #
Подобное я уже где-то видел… А, в плагине Lightbox для RightJS: rightjs.org/builds/ui/right-lightbox.js
0
lol2Fast4U #
«var self = this», кстати, помогает, если ночью, когда «мозги не варят», переключаешься между Python и JS кодом и путаешься %)
0
Mourner #
Я для своего Санкалка на 10к использовал ту же методику. :) Всё-таки удалось в итоге добить размер до заветных 10240. Основной прирост при это обеспечил модуль Math и все его методы — Math -> m, m.cos -> mc, etc. :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.