Pull to refresh

Маньячная минимизация (в погоне за байтом)

JavaScript *
Hello World,

Этот топик о том, каким образом можно предварительно зарефакторить код так, чтобы улучшить его минимизацию. Недавно я перед релизом минимизировал библиотеку Helios Kernel (о которой написал позавчера). Исходник библиотеки весит 28112 байт, в нём щедрые комментарии, и поэтому он с пол пинка ужимается YUI компрессором до 7083 байт. Не то что бы мне показалось, что 7 килобайт — слишком жирно. Но просто, посмотрев своими глазами на минимизированный код, я смог увидеть кучу мест, где можно было бы сэкономить ещё:



Посмотрим, что можно сделать с кодом, чтобы превратить 7083 байт в 4009 3937.

Но прежде чем начать, две оговорки:
  • Мы не будем использовать всякие «грязные» трюки (вроде var a=this или var f=false), которые теоретически приводят к замедлению. Предполагается, что быстродействие всё же важнее размера файла.
  • На каждом шаге я прогонял код через набор тестов. Часто бывает так, что после какого-то изменения всё перестаёт работать. Если в процессе ручной оптимизации вы не будете тестировать код (или если у вас вообще тестов нет), тогда тот код, который получится в итоге, скорее всего работать не будет.


Выбор минимайзера


Вообще, эта статья не про сравнение минимайзеров, но в процессе я заметил, что у YUI компрессора есть баг фича: он не убирает фигурные скобки у блоков кода, состящих из одной строки. Более того, он добавляет фигурные скобки, даже если в оригинале их не было (на первой картинке помечено тегом WTF). Я воспринял это как хамство и, не долго думая, перешёл на использование онлайн-минимайзера http://jscompress.com/. Впрочем, остальные рассуждения применимы к любому минимайзеру на ваш вкус.

Большая Анонимная Функция


Для начала, давайте обернём весь код в большую анонимную функцию, которая будет тут же вызываться (если изначально этого не сделано). Тогда мы сможем пользоваться локальной областью видимости этой функции. Как это позволит сэкономить байты, будет показано ниже. Самый компактный способ оборачивания кода в анонимную функцию выглядит так:
было стало
// код


!function(){
    // код
}()

«Приватные» объекты


Наверняка в коде имеется большое количество вспомогательных объектов, которые не включены в публичный API. Поскольку в Javascript нет нативного способа указать, что объект является приватным, обычно пользуются каким-то соглашением. Чаще всего такие объекты именуют начиная с символа подчёркивания: "_". Обычно минимайзер заменяет имена локальных переменных на однобуквенные, но оставляет неизменными имена «приватных» объектов, потому что не делает смелых предположений, относительно того, как мы обозначаем «приватные» объекты. Но нам не важно, как эти объекты будут называться в минимизированном коде, поэтому можно переименовать их вручную:
было стало
myObject._somethingPrivate = {
    // ...
}
myObject.a = {
    // ...
}
MyObj = function() {
    this.somePublicProperty = ...;
    this._somePrivateProperty = ...;
    this._anotherPrivateProperty = ...;
}
MyObj = function() {
    this.somePublicProperty = ...;
    this.a = ...;
    this.b = ...;
}
MyObj.prototype._privateMethod = function() {
    // ...
}
MyObj.prototype.c = function() {
    // ...
}

Здесь нужно быть аккуратным. Во-первых, не забывайте заменять имена приватных функций и переменных не только в объявлениях, но и там, где они используются. Во-вторых, нужно отслеживать логику кода, и не допускать пересечений имён. Например, если у какого-то типа в прототипе уже объявлена функция a, нельзя таким же именем называть приватное свойство этого объекта. Это очевидная вещь, но её легко упустить, если не обращать на это специального внимания.

Кроме того, приватные объекты часто объявляются не только во всяких конструкторах/инициализаторах. Javascript позволяет дополнять объекты налету. По идее, все приватные идентификаторы в коде можно аккуратно позаменять на однобуквенные:
было стало
MyObj.prototype.getSomething = function() {
    if ( typeof this._prop == "undefined" ) {
        this._prop = 0;
    }

    return this._prop;
}
MyObj.prototype.getSomething = function() {
    if ( typeof this.x == "undefined" ) {
        this.x = 0;
    }

    return this.x;
}

«Публичные» объекты


«Публичные» объекты — это такие, которые входят в API, и нам нужно, чтобы они назывались именно так, как они были названы изначально. Но если «публичный» объект используется внутри кода слишком часто (ну, скажем, хотя бы один раз), а его имя слишком длинное (ну, скажем, больше двух байт), тогда имеет смысл сделать ему алиас:
было стало
myObject = { ... }
var a = myObject = { ... }

В этом примере после такого изменения, переменная a будет объявлена как локальная, а переменная myObject — как глобальная (при условии, что идентификатор myObject используется впервые.

Теперь можно пробежаться по коду, найти все объекты, которые не только объявляются, но и используются, и сделать им алиас:
было стало
MyObj = function() {
    this.somePublicProperty = ...;
    this.a = ...;
    this.b = ...;
}
var b = MyObj = function() {
    this.somePublicProperty = ...;
    this.a = ...;
    this.b = ...;
}
MyObj.prototype.someMethod = function() {
    // ...
}
b.prototype.d = b.prototype.someMethod = function() {
    // ...
}
someStorage.someMethod = function() {
    // ...
}
var c = someStorage.someMethod = function() {
    // ...
}

И опять, главное не запутаться в областях видимости и не называть переменные из одной области видимости одинаковыми именами. В примерах выше, у объекта типа MyObj уже есть приватное свойство b и приватный метод c, а новые локальные переменные b и c попадают в область видимости Большой Анонимной Функции, в которую мы обернули весь код в самом начале (обернули же, правда? не забыли?)

Кроме того, мы можем сделать алиасы некоторым публичным свойствам, но только тем, которые содержат сложные объекты:
было стало
AnotherObj = function() {
    this.someProperty = [ 0, 0, 0 ]; // массив
    this.secondProperty = { a: 1 }; // хэш
    this.thirdProperty = 0; // число
    this.fourthProperty = true; // буль-буль
    this.fifthProperty = "hello"; // строка
}
AnotherObj = function() {
    this.a = this.someProperty = [ 0, 0, 0 ];
    this.b = this.secondProperty = { a: 1 };
    this.thirdProperty = 0;
    this.fourthProperty = true;
    this.fifthProperty = "hello";
}

Если мы сделаем алиасы для простых объектов, это приведёт к копированию содержимого, и алиас будет указывать на другой объект.

Собираем var


Теперь воспользуемся тем, что можно объявлять переменные через запятую, используя слово var один раз. В простейшем случае, это выглядит так:
было стало
someFunction = function() {
    var a = 0;
    var b = something();
    // ...
}
someFunction = function() {
    var a = 0, b = something();
    // ...
}

anotherFunction = function() {
    var c;
    // какой-то код
    var d = something();
    // ещё какой-то код
    for ( var i = 0; i < ...
    // и ещё какой-нибудь код
}
anotherFunction = function() {
    var c, d = something(), i = 0
    // какой-то код
    // ещё какой-то код
    for ( ; i < ...
    // и ещё какой-нибудь код
}


В общем, нужно вытащить все объявления в начало функции и написать их с использованием одного var. Про оптимизацию цикла for() я напишу ниже. И ещё нужно собрать все локальные объявления внутри нашей Большой Адронной Функции и тоже засунуть их под один var в начале. Это как раз те алиасы, которые мы насоздавали в предыдущем разделе. Весь код должен преобразоваться примерно таким образом:
было стало
!function(){

    // какой-то код

    var b = MyObj = function() {
        this.somePublicProperty = ...;
        this.a = ...;
        this.b = ...;
    }

    // ещё какой-то код

    var c = b.prototype.someMethod = function() {
        // ...
    }

    // и ещё какой-нибудь код

}()

!function(){
    var  b = MyObj = function() {
        this.somePublicProperty = ...;
        this.a = ...;
        this.b = ...;
    },

    c = b.prototype.someMethod = function() {
        // ...
    },

    // и так со всеми алиасами

    // какой-то код

    // ещё какой-то код

    // и ещё какой-нибудь код

}()

Обратите внимание, что в этом примере переменные b, c и им подобные остаются объявлены как локальные для Большой Функции. Таким образом, мы сэкономим столько var'ов, сколько их было в функции (ну, кроме одного).

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

Прототипы


Для каждого объявленного типа и его конструктора можно нехило сэкономить на слове protoype — уж слишком оно длинное. Для этого опишем весь прототип для будущих объектов этого типа в виде одного хэша:
было стало
MyObj = function() {
    // ...
}

MyObj.prototype.someMethod = function() {
    // ...
}

MyObj.prototype.anotherMethod = function() {
    // ...
}

MyObj.prototype.thirdMethod = function() {
    // ...
}


MyObj = function() {
    // ...
}

MyObj.prototype = {
    someMethod : function() {
        // ...
    },

    anotherMethod : function() {
        // ...
    },

    thirdMethod : function() {
        // ...
    }
}

Как видно, для этого нужно не забыть позаменять "=" на ":" и разделить объявления методов запятыми. Этот способ не сработает для случая, когда нужно дополнить какой-то прототип для конструктора типа, объявленного где-то в другом месте (потому что такой записью мы полностью переопределяем прототип).

Оптимизация циклов и условий


Почти все циклы и многие условия можно оптимизировать:
было стало
a--;
if ( a == 0 ) {
    // ...
}
if ( --a == 0 ) {
    // ...
}

if ( --a == 0 ) {
    // ...
}
if ( !--a ) {
    // ...
}
for ( var i = 0; i < a; i++ ) {
    b( c[ i ] );
}
for ( var i = 0; i < a; ) {
    b( c[ i++ ] );
}

Но здесь тоже нужно быть внимательным, чтоб не нарушить логику кода.

Частоиспользуемые значения


Бывает, что есть значения, которые используются больше одного раза. Их тоже можно вынести в переменные:
было стало
// ...
if ( typeof a == "undefined" ) ...
// ...
if ( typeof b == "undefined" ) ...
// ...

var z = "undefined";
// ...
if ( typeof a == z ) ...
// ...
if ( typeof b == z ) ...
// ...
if ( typeof a != "function" ) {
    a = function(){}
}
// ...
if ( typeof b != "function" ) {
    b = function(){}
}


var f = "function", g = function(){}
// ...
if ( typeof a != f ) {
    a = g;
}
// ...
if ( typeof b != f ) {
    b = g;
}
el = document.createElement( "script" );
el.type = "text/javascript";

var x = "script";
el = document.createElement( x );
el.type = "text/java" + x;


Выбрасываем всё лишнее


Часто бывает, что код содержит лишнюю информацию «для ясности», от которой можно избавиться. Но здесь, как и везде, нужно внимательно отслеживать, что мы удаляем:
было стало
if ( a.length > 0 ) {
    b = a.pop()
}
if ( a.length ) {
    b = a.pop()
}
var someEnum = { foo : 0, bar : 1, buz : 2 }

// ...

var a = [];
for ( var i in someEnum ) {
    a[ someEnum[ i ] ] = 0;
}

// ...

a[ someEnum.bar ] = getSomething();

// ...

if ( c.state == someEnum.foo ) {
    // ...
}
var someEnum = { foo : 0, bar : 1, buz : 2 }

// ...

var a = [ 0, 0, 0 ];




// ...

a[ 1 ] = getSomething();

// ...

if ( !c.state ) {
    // ...
}

Бонус: убираем var


Это интересный трюк, который пригоден в тех случаях, когда внутри функции объявлена одна локальная переменная (или если переменная объявляется без инициализации). Здесь мы экономим на одном var'е, но нам приходится дублировать имя переменной:
было стало
doSomething = function( param1, param2 ) {
    var i = 0;
    // ....
}
doSomething = function( param1, param2, i ) {
    i = 0;
    // ....
}
doSomething = function( param1, param2 ) {
    var a, b, c;
    // ....
}
doSomething = function( param1, param2, a, b, c ) {
    // ....
}


Здесь мы используем параметры вместо локальных переменных, но ведут себя они точно так же. Этот трюк не пригоден в тех случаях, когда функция принимает не известное заранее число параметров. Чаще всего он позволяет избавится от почти всех var'ов в коде.

Что получилось в итоге


После обработки кода описанными способами, я скормил скрипт сервису jscompress.com. Немного подумав, он выдал мне вот такую кашу на 4009 байт. Приятного аппетита!



Кстати, я раздам плюсы в карму тем, кто найдёт и опишет в комментах, что ещё можно урезать в этой каше :-)

Update

nano_freelancer предложил несколько правильных идей:
  • позаменять все true и false на 1 и 0 соответственно
  • for (initial;condition;loop statement) {statements}
    можно после loop statement поставить запятую и расположить все операторы из statements через запятую(вместо точки с запятой) — экономим 2 байта (фигурные скобки). Но это применимо только для случаев, когда statement само не содержит сложных операторов.

Кроме того, большинство null'ов также можно заменить на 0 (но не все).

Размер кода уменьшен до 3937 байт :-)

Оффтопик: исходный и минимизированный коды, с которыми я работал, доступны для скачивания на домашней страничке проекта: http://home.gna.org/helios/kernel/
Tags:
Hubs:
Total votes 171: ↑154 and ↓17 +137
Views 7.7K
Comments Comments 121