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

Посмотрим, что можно сделать с кодом, чтобы превратить 7083 байт в4009 3937.
Но прежде чем начать, две оговорки:
Вообще, эта статья не про сравнение минимайзеров, но в процессе я заметил, что у YUI компрессора естьбаг фича: он не убирает фигурные скобки у блоков кода, состящих из одной строки. Более того, он добавляет фигурные скобки, даже если в оригинале их не было (на первой картинке помечено тегом WTF). Я воспринял это как хамство и, не долго думая, перешёл на использование онлайн-минимайзера http://jscompress.com/. Впрочем, остальные рассуждения применимы к любому минимайзеру на ваш вкус.
Для начала, давайте обернём весь код в большую анонимную функцию, которая будет тут же вызываться (если изначально этого не сделано). Тогда мы сможем пользоваться локальной областью видимости этой функции. Как это позволит сэкономить байты, будет показано ниже. Самый компактный способ оборачивания кода в анонимную функцию выглядит так:
Наверняка в коде имеется большое количество вспомогательных объектов, которые не включены в публичный API. Поскольку в Javascript нет нативного способа указать, что объект является приватным, обычно пользуются каким-то соглашением. Чаще всего такие объекты именуют начиная с символа подчёркивания: "_". Обычно минимайзер заменяет имена локальных переменных на однобуквенные, но оставляет неизменными имена «приватных» объектов, потому что не делает смелых предположений, относительно того, как мы обозначаем «приватные» объекты. Но нам не важно, как эти объекты будут называться в минимизированном коде, поэтому можно переименовать их вручную:
Здесь нужно быть аккуратным. Во-первых, не забывайте заменять имена приватных функций и переменных не только в объявлениях, но и там, где они используются. Во-вторых, нужно отслеживать логику кода, и не допускать пересечений имён. Например, если у какого-то типа в прототипе уже объявлена функция a, нельзя таким же именем называть приватное свойство этого объекта. Это очевидная вещь, но её легко упустить, если не обращать на это специального внимания.
Кроме того, приватные объекты часто объявляются не только во всяких конструкторах/инициализаторах. Javascript позволяет дополнять объекты налету. По идее, все приватные идентификаторы в коде можно аккуратно позаменять на однобуквенные:
«Публичные» объекты — это такие, которые входят в API, и нам нужно, чтобы они назывались именно так, как они были названы изначально. Но если «публичный» объект используется внутри кода слишком часто (ну, скажем, хотя бы один раз), а его имя слишком длинное (ну, скажем, больше двух байт), тогда имеет смысл сделать ему алиас:
В этом примере после такого изменения, переменная a будет объявлена как локальная, а переменная myObject — как глобальная (при условии, что идентификатор myObject используется впервые.
Теперь можно пробежаться по коду, найти все объекты, которые не только объявляются, но и используются, и сделать им алиас:
И опять, главное не запутаться в областях видимости и не называть переменные из одной области видимости одинаковыми именами. В примерах выше, у объекта типа MyObj уже есть приватное свойство b и приватный метод c, а новые локальные переменные b и c попадают в область видимости Большой Анонимной Функции, в которую мы обернули весь код в самом начале (обернули же, правда? не забыли?)
Кроме того, мы можем сделать алиасы некоторым публичным свойствам, но только тем, которые содержат сложные объекты:
Если мы сделаем алиасы для простых объектов, это приведёт к копированию содержимого, и алиас будет указывать на другой объект.
Теперь воспользуемся тем, что можно объявлять переменные через запятую, используя слово var один раз. В простейшем случае, это выглядит так:
В общем, нужно вытащить все объявления в начало функции и написать их с использованием одного var. Про оптимизацию цикла for() я напишу ниже. И ещё нужно собрать все локальные объявления внутри нашей Большой Адронной Функции и тоже засунуть их под один var в начале. Это как раз те алиасы, которые мы насоздавали в предыдущем разделе. Весь код должен преобразоваться примерно таким образом:
Обратите внимание, что в этом примере переменные b, c и им подобные остаются объявлены как локальные для Большой Функции. Таким образом, мы сэкономим столько var'ов, сколько их было в функции (ну, кроме одного).
И ещё нужно следить, чтобы не поменялась логика кода. Мы ведь меняем порядок строк, поэтому теоретически может произойти так, что какой-то объект будет использоваться до того, как он проинициализирован, этого нельзя допустить.
Для каждого объявленного типа и его конструктора можно нехило сэкономить на слове protoype — уж слишком оно длинное. Для этого опишем весь прототип для будущих объектов этого типа в виде одного хэша:
Как видно, для этого нужно не забыть позаменять "=" на ":" и разделить объявления методов запятыми. Этот способ не сработает для случая, когда нужно дополнить какой-то прототип для конструктора типа, объявленного где-то в другом месте (потому что такой записью мы полностью переопределяем прототип).
Почти все циклы и многие условия можно оптимизировать:
Но здесь тоже нужно быть внимательным, чтоб не нарушить логику кода.
Бывает, что есть значения, которые используются больше одного раза. Их тоже можно вынести в переменные:
Часто бывает, что код содержит лишнюю информацию «для ясности», от которой можно избавиться. Но здесь, как и везде, нужно внимательно отслеживать, что мы удаляем:
Это интересный трюк, который пригоден в тех случаях, когда внутри функции объявлена одна локальная переменная (или если переменная объявляется без инициализации). Здесь мы экономим на одном var'е, но нам приходится дублировать имя переменной:
Здесь мы используем параметры вместо локальных переменных, но ведут себя они точно так же. Этот трюк не пригоден в тех случаях, когда функция принимает не известное заранее число параметров. Чаще всего он позволяет избавится от почти всех var'ов в коде.
После обработки кода описанными способами, я скормил скрипт сервису jscompress.com. Немного подумав, он выдал мне вот такую кашу на 4009 байт. Приятного аппетита!

Кстати, я раздам плюсы в карму тем, кто найдёт и опишет в комментах, что ещё можно урезать в этой каше :-)
nano_freelancer предложил несколько правильных идей:
Кроме того, большинство null'ов также можно заменить на 0 (но не все).
Размер кода уменьшен до 3937 байт :-)
Оффтопик: исходный и минимизированный коды, с которыми я работал, доступны для скачивания на домашней страничке проекта: http://home.gna.org/helios/kernel/
Этот топик о том, каким образом можно предварительно зарефакторить код так, чтобы улучшить его минимизацию. Недавно я перед релизом минимизировал библиотеку


Посмотрим, что можно сделать с кодом, чтобы превратить 7083 байт в
Но прежде чем начать, две оговорки:
- Мы не будем использовать всякие «грязные» трюки (вроде var a=this или var f=false), которые теоретически приводят к замедлению. Предполагается, что быстродействие всё же важнее размера файла.
- На каждом шаге я прогонял код через набор тестов. Часто бывает так, что после какого-то изменения всё перестаёт работать. Если в процессе ручной оптимизации вы не будете тестировать код (или если у вас вообще тестов нет), тогда тот код, который получится в итоге, скорее всего работать не будет.
Выбор минимайзера
Вообще, эта статья не про сравнение минимайзеров, но в процессе я заметил, что у YUI компрессора есть
Большая Анонимная Функция
Для начала, давайте обернём весь код в большую анонимную функцию, которая будет тут же вызываться (если изначально этого не сделано). Тогда мы сможем пользоваться локальной областью видимости этой функции. Как это позволит сэкономить байты, будет показано ниже. Самый компактный способ оборачивания кода в анонимную функцию выглядит так:
было | стало |
---|---|
|
|
«Приватные» объекты
Наверняка в коде имеется большое количество вспомогательных объектов, которые не включены в публичный API. Поскольку в Javascript нет нативного способа указать, что объект является приватным, обычно пользуются каким-то соглашением. Чаще всего такие объекты именуют начиная с символа подчёркивания: "_". Обычно минимайзер заменяет имена локальных переменных на однобуквенные, но оставляет неизменными имена «приватных» объектов, потому что не делает смелых предположений, относительно того, как мы обозначаем «приватные» объекты. Но нам не важно, как эти объекты будут называться в минимизированном коде, поэтому можно переименовать их вручную:
было | стало |
---|---|
|
|
|
|
|
|
Здесь нужно быть аккуратным. Во-первых, не забывайте заменять имена приватных функций и переменных не только в объявлениях, но и там, где они используются. Во-вторых, нужно отслеживать логику кода, и не допускать пересечений имён. Например, если у какого-то типа в прототипе уже объявлена функция a, нельзя таким же именем называть приватное свойство этого объекта. Это очевидная вещь, но её легко упустить, если не обращать на это специального внимания.
Кроме того, приватные объекты часто объявляются не только во всяких конструкторах/инициализаторах. Javascript позволяет дополнять объекты налету. По идее, все приватные идентификаторы в коде можно аккуратно позаменять на однобуквенные:
было | стало |
---|---|
|
|
«Публичные» объекты
«Публичные» объекты — это такие, которые входят в API, и нам нужно, чтобы они назывались именно так, как они были названы изначально. Но если «публичный» объект используется внутри кода слишком часто (ну, скажем, хотя бы один раз), а его имя слишком длинное (ну, скажем, больше двух байт), тогда имеет смысл сделать ему алиас:
было | стало |
---|---|
|
|
В этом примере после такого изменения, переменная a будет объявлена как локальная, а переменная myObject — как глобальная (при условии, что идентификатор myObject используется впервые.
Теперь можно пробежаться по коду, найти все объекты, которые не только объявляются, но и используются, и сделать им алиас:
было | стало |
---|---|
|
|
|
|
|
|
И опять, главное не запутаться в областях видимости и не называть переменные из одной области видимости одинаковыми именами. В примерах выше, у объекта типа MyObj уже есть приватное свойство b и приватный метод c, а новые локальные переменные b и c попадают в область видимости Большой Анонимной Функции, в которую мы обернули весь код в самом начале (обернули же, правда? не забыли?)
Кроме того, мы можем сделать алиасы некоторым публичным свойствам, но только тем, которые содержат сложные объекты:
было | стало |
---|---|
|
|
Если мы сделаем алиасы для простых объектов, это приведёт к копированию содержимого, и алиас будет указывать на другой объект.
Собираем var'ы
Теперь воспользуемся тем, что можно объявлять переменные через запятую, используя слово var один раз. В простейшем случае, это выглядит так:
было | стало |
---|---|
|
|
|
|
В общем, нужно вытащить все объявления в начало функции и написать их с использованием одного var. Про оптимизацию цикла for() я напишу ниже. И ещё нужно собрать все локальные объявления внутри нашей Большой Адронной Функции и тоже засунуть их под один var в начале. Это как раз те алиасы, которые мы насоздавали в предыдущем разделе. Весь код должен преобразоваться примерно таким образом:
было | стало |
---|---|
|
|
Обратите внимание, что в этом примере переменные b, c и им подобные остаются объявлены как локальные для Большой Функции. Таким образом, мы сэкономим столько var'ов, сколько их было в функции (ну, кроме одного).
И ещё нужно следить, чтобы не поменялась логика кода. Мы ведь меняем порядок строк, поэтому теоретически может произойти так, что какой-то объект будет использоваться до того, как он проинициализирован, этого нельзя допустить.
Прототипы
Для каждого объявленного типа и его конструктора можно нехило сэкономить на слове protoype — уж слишком оно длинное. Для этого опишем весь прототип для будущих объектов этого типа в виде одного хэша:
было | стало |
---|---|
|
|
Как видно, для этого нужно не забыть позаменять "=" на ":" и разделить объявления методов запятыми. Этот способ не сработает для случая, когда нужно дополнить какой-то прототип для конструктора типа, объявленного где-то в другом месте (потому что такой записью мы полностью переопределяем прототип).
Оптимизация циклов и условий
Почти все циклы и многие условия можно оптимизировать:
было | стало |
---|---|
|
|
|
|
|
|
Но здесь тоже нужно быть внимательным, чтоб не нарушить логику кода.
Частоиспользуемые значения
Бывает, что есть значения, которые используются больше одного раза. Их тоже можно вынести в переменные:
было | стало |
---|---|
|
|
|
|
|
|
Выбрасываем всё лишнее
Часто бывает, что код содержит лишнюю информацию «для ясности», от которой можно избавиться. Но здесь, как и везде, нужно внимательно отслеживать, что мы удаляем:
было | стало |
---|---|
|
|
|
|
Бонус: убираем var'ы
Это интересный трюк, который пригоден в тех случаях, когда внутри функции объявлена одна локальная переменная (или если переменная объявляется без инициализации). Здесь мы экономим на одном var'е, но нам приходится дублировать имя переменной:
было | стало |
---|---|
|
|
|
|
Здесь мы используем параметры вместо локальных переменных, но ведут себя они точно так же. Этот трюк не пригоден в тех случаях, когда функция принимает не известное заранее число параметров. Чаще всего он позволяет избавится от почти всех var'ов в коде.
Что получилось в итоге
После обработки кода описанными способами, я скормил скрипт сервису jscompress.com. Немного подумав, он выдал мне вот такую кашу на 4009 байт. Приятного аппетита!

Кстати, я раздам плюсы в карму тем, кто найдёт и опишет в комментах, что ещё можно урезать в этой каше :-)
Update
nano_freelancer предложил несколько правильных идей:
- позаменять все true и false на 1 и 0 соответственно
можно после loop statement поставить запятую и расположить все операторы из statements через запятую(вместо точки с запятой) — экономим 2 байта (фигурные скобки). Но это применимо только для случаев, когда statement само не содержит сложных операторов.for (initial;condition;loop statement) {statements}
Кроме того, большинство null'ов также можно заменить на 0 (но не все).
Размер кода уменьшен до 3937 байт :-)
Оффтопик: исходный и минимизированный коды, с которыми я работал, доступны для скачивания на домашней страничке проекта: http://home.gna.org/helios/kernel/