Техника минификации JS кода
Сентябрь 20, 2015
Недавно для одного проекта я портанул Лайкли на ванильный JS. Причина была в том что мне не хотелось тащить jQuery в свой проект ради социальных кнопок, но кнопки были нужны.
Тащить jQuery ради одного плагина не очень хотелось, по этому я решил потратить немножко времени на перевод Лайкли на ванильный JS. За время которое я провел портатируя Лайкли на ванильный JS я узнал несколько трюков и технику по минификации JS кода у минификатора кода uglify-js.
Внимание: профессионал в действие, и не повторяйте это минифицируйте
ничего дома. Дополнительные скобки, комментарии и отступы добавлены с целью
повышения читабельности сжатого кода.
Сжатие инструкций
Начнем с самого простого. Самое простое — это сжать несколько
инструкций
в одну инструкцию. Сжимая инструкции, минифиактор может сократить код от одной
;
и фигурных скобок.
Чтобы сжать несколько инструкций вроде вызовов функций/методов, присваивание
значения к уже определенной переменной или другой операции можно воспользоватся
,
.
Было 3 отдельных инструкций и вызова к 3-ем методам.
this.getUserInput();
this.processUserInput();
this.doSomethingElse();
Стало тех же самых три вызова, только теперь это все в одной инструкции и при
этом сэкономили один символ (;
).
this.getUserInput(),
this.processUserInput(),
this.doSomethingElse()
Выигрыш небольшой, зато чем больше функций, тем больше экономим на спичках.
Сжатие инструкций и return
Сжатие инструкций в функции с return
делается немножко по другому. Допустим,
есть код который вызывает несколько функций и возвращает результат.
/**
* Типичная инициализация игрового движка на WebGL
*
* @return {WebGLRenderingContext}
*/
function init () {
initCanvas();
initGraphicContext();
initShaders();
initBuffers();
return getContext();
}
Чтобы сжать инструкции в этой функции, достаточно переместить все вызовы внутрь
return
, сжать инструкции в одну инструкцию и добавить в конец то что мы хотим
возвратить из функции.
/** ... */
function init () {
return initCanvas(),
initGraphicContext(),
initShaders(),
initBuffers(),
getContext()
}
Запятые в JS работают не только для разделения аргументов в функциях, пары
ключей-значений в ассоциативных массивах и элементы в массивах. С помощью
запятых, можно выполнить несколько выражений в одной инструкции. А результат
последнего выражения в инструкции будет возвращен (как getContext()
в примере
выше).
В данном случае, выигрыш будет в один символ. Далее следует более продвинутые варианты экономии места.
Сжатие if-else
Переходим от простого к более продвинутому способу сжатию кода. Сжать простую if-else конструкцию можно с помощью тернарного оператора.
if (is_user_logged_in()) {
greet_user();
}
else {
kick_out_user();
}
Нечитабельно, зато сэкономили на фигурных скобках и ключевых словах.
is_user_logged_in() ? greet_user() : kick_out_user();
if-elseif-else сжимается таким же образом только с вложенным тернарным оператором.
if (is_user_admin()) {
greet_admin();
}
else if (is_user_logged_in()) {
greet_user();
}
else {
kick_out_user();
}
В итоге получается вот такая нечитабельная конструкция.
is_user_admin() ? greet_admin : (is_user_logged_in() ? greet_user() : kick_out_user());
Главное чтобы работало, не так ли? С тернарными операторами можно сэкономить намного больше места в сравнение с сжатия инструкций.
Сжатие одного if
if-(elseif)-else сжимаются с помощью тернарных выражений в то время как if сам
по себе (без else и else if) сжимается через логические операторы &&
и ||
.
Вот фрагмент кода из минифицированного Лайкли:
/**
* Получить DOM узел счетчика.
* Если DOM узел счетчика еще не существует то его надо создать.
*
* @return {jQuery}
*/
getCounterElem: function() {
var e = this.widget.find("." + u + "counter_single");
return e.length || (e = t("<span>", {
"class": c("counter", "single")
}), this.widget.append(e)), e
}
Обратите внимание на длинное выражение после return
. e.length || (e = ...)
это то самое место где сжат if
. Тут весь прикол в логике и ||
операторе.
А теперь тот же самый фрагмент кода, только более читабельно и с if
.
/** ... */
getCounterElem: function() {
var counter = this.widget.find("." + prefix + "counter_single");
// e.length || ...
if (!counter.length) {
counter = $("<span>", {
"class": createClass("counter", "single")
});
this.widget.append(counter);
}
return counter;
}
Другой пример сжатия if
только с &&
.
/**
* Получить все data-* значения, обработать значения и
* сохранить все это ассоциативный массив this.options
*/
detectParams: function() {
var t = this.widget.data();
if (t.counter) {
var e = parseInt(t.counter, 10);
isNaN(e) ? this.options.counterUrl = t.counter : this.options.counterNumber = e
}
t.title && (this.options.title = t.title), t.url && (this.options.url = t.url)
}
Тут используется &&
для сжатия двух if
в одну инструкцию. Заметьте,
if (t.counter)
и все что внутри этого if
не может быть сжато в одну
инструкцию т.к. внутри блока имеется объявление переменной e
. Определение
переменных нельзя сжимать, только переназначение уже определенных переменых
можно сжать.
А читабельный вариант будет выглядить так:
/** ... */
detectParams: function() {
var data = this.widget.data();
if (data.counter) {
var counter = parseInt(data.counter, 10);
if (isNaN(counter)) {
this.options.counterUrl = data.counter;
}
else {
this.options.counterNumber = counter;
}
}
// t.title && ...
if (data.title) {
this.options.title = data.title;
}
// t.url && ...
if (data.url) {
this.options.url = data.url;
}
}
Читабельная версия выглядит намного лучше, но длинее. На &&
и ||
можно
сэкономить много места.
Заметка: uglify-js убирает условия если эти условия содержут константные
значения и оптимзирует иногда условия внутри if
чтобы сэкономить побольше
места.
Сжатие циклов
Циклы сжимаются посредством удаления фигурных скобок и сжатием всех выражений внутри методом который описан выше. Также некоторые минификаторы могут оптимизировать сжатие в некоторых ситуациях.
Вот небольшой абстрактный пример сжатия цикла.
var success = false;
while (!success) {
if (keep_trying() && is_succeeded()) {
success = true;
}
eat();
sleep();
}
Тут мы опустим фигурные скобки (для одной инструкции они не обязательны),
сожмем if
с помощью &&
и воспользуемся ,
для сжатия всего тела цикла в
одну инструкцию.
var success = false;
while(!success)keep_trying() && is_succeeded() && (success = true),eat(),sleep()
Тут нету ничего сложного. Единственное на чем можно сэкономить в циклах это на
фигурных скобках и, в зависимости от ситуации, пару символов на while
.
Сжатие литералов
true
, false
и undefined
тоже можно сжать. true
и false
сжимается
посредством опретаором !
. Вот простая функция которая определяет является ли
переменная булевом.
function is_bool (bool) {
return bool === true || bool === false;
}
Чтобы сжать true
и false
достаточно воспользоватся оператором !
и 0
или 1
:
/**
* !0 === true
* !1 === false
*/
function is_bool (bool) {
return bool === !0 || bool === !1;
}
А вот undefined
можно сжать двумя способами: если есть замыкание через
аргумент, через свойство объекта или же через выражение void 0
.
Сжатие undefined
через замыкание требует определения аргумента в замыкание,
который никогда не будет передан, что сделает его undefined
. Этот метод
требует минификатор или замену undefined
на имя переменной которой вы
дали для замены undefined
в аргументах замыкания.
(function (undefined) {
if (a == undefined) {
a = b == undefined;
}
else {
a = undefined;
}
})();
И после прогона через минификатор вроде uglify-js мы получим сильно неузнаваемый укороченный код.
!function(n){a==n?a=b==n:a=n}()
Выглядит сногсшибательно.
Заключение
Данный пост описывает только некоторые техники минификации кода. uglify-js
делает еще многого всего включая грамотно сжимать название переменных в одну
букву (), удаление комментариев и лишних whitespace (
, \n
, \t
) символов.
У минификатор также имеются некоторые ограничения, например: минификаторы не
могут сжимать ключи ассоцитивных массивов при определение внутри {}
литералов
или же при доступе к свойству. Можно было бы сэкономить много места на внутреней
API сжимая имена всех методов и свойств.
- JavaScript
- js uglify-js минификация
Комментарии