Техника минификации JS кода

Недавно для одного проекта я портанул Лайкли на ванильный 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 сжимая имена всех методов и свойств.

Поделится

Комментарии