# Как работают сжиматели JavaScript Перед выкладыванием JavaScript на "боевую" машину -- пропускаем его через минификатор (также говорят "сжиматель"), который удаляет пробелы и по-всякому оптимизирует код, уменьшая его размер. В этой статье мы посмотрим, как работают современные минификаторы, за счёт чего они укорачивают код и какие с ними возможны проблемы. [cut] ## Современные сжиматели Рассматриваемые в этой статье алгоритмы и подходы относятся к минификаторам последнего поколения. Вот их список: Самые широко используемые -- первые два, поэтому будем рассматривать в первую очередь их. Наша цель -- понять, как они работают, и что интересного с их помощью можно сотворить. ## С чего начать? Для GCC:
  1. Убедиться, что стоит [Java](http://java.oracle.com)
  2. Скачать и распаковать [](http://closure-compiler.googlecode.com/files/compiler-latest.zip), нам нужен файл `compiler.jar`.
  3. Сжать файл `my.js`: `java -jar compiler.jar --charset UTF-8 --js my.js --js_output_file my.min.js`
Обратите внимание на флаг `--charset` для GCC. Без него русские буквы будут закодированы во что-то типа `\u1234`. Google Closure Compiler также содержит [песочницу](http://closure-compiler.appspot.com/home) для тестирования сжатия и [веб-сервис](https://developers.google.com/closure/compiler/docs/gettingstarted_api?hl=ru), на который код можно отправлять для сжатия. Но скачать файл обычно гораздо проще, поэтому его редко где используют. Для UglifyJS:
  1. Убедиться, что стоит [Node.js](http://nodejs.org)
  2. Поставить `npm install -g uglify-js`.
  3. Сжать файл `my.js`: `uglifyjs my.js -o my.min.js`
## Что делает минификатор? Все современные минификаторы работают следующим образом:
  1. Разбирают JavaScript-код в синтаксическое дерево. Также поступает любой интерпретатор JavaScript перед тем, как его выполнять. Но затем, вместо исполнения кода...
  2. Бегают по этому дереву, анализируют и оптимизируют его.
  3. Записывают из синтаксического дерева получившийся код.
## Как выглядит дерево? Посмотреть синтаксическое дерево можно, запустив компилятор со специальным флагом. Для GCC есть даже способ вывести его:
  1. Сначала сгенерируем дерево в формате [DOT](http://en.wikipedia.org/wiki/DOT_language): ``` java -jar compiler.jar --js my.js --use_only_custom_externs --print_tree >my.dot ``` Здесь флаг `--print_tree` выводит дерево, а `--use_only_custom_externs` убирает лишнюю служебную информацию.
  2. Файл в этом формате используется в различных программах для графопостроения. Чтобы превратить его в обычную картинку, подойдёт утилита `dot` из пакета [Graphviz](http://www.graphviz.org/): ``` // конвертировать в формат png dot -Tpng my.dot -o my.png // конвертировать в формат svg dot -Tsvg my.dot -o my.svg ```
Пример кода `my.js`: ```js function User(name) { this.sayHi = function() { alert( name ); }; } ``` Результат, получившееся из `my.js` дерево: В узлах-эллипсах на иллюстрации выше стоит тип, например `FUNCTION` (функция) или `NAME` (имя переменной). Комментарии к ним на русском языке добавлены мной вручную. Кроме него к каждому узлу привязаны конкретные данные. Сжиматель умеет ходить по этому дереву и менять его, как пожелает. [smart header="Комментарии JSDoc"] Обычно когда код превращается в дерево -- из него естественным образом исчезают комментарии и пробелы. Они не имеют значения при выполнении, поэтому игнорируются. Но Google Closure Compiler добавляет в дерево информацию из *комментариев JSDoc*, т.е. комментариев вида `/** ... */`, например: ```js *!* /** * Номер минимальной поддерживаемой версии IE * @const * @type {number} */ */!* var minIEVersion = 8; ``` Такие комментарии не создают новых узлов дерева, а добавляются в качестве информации к существующем. В данном случае -- к переменной `minIEVersion`. В них может содержаться информация о типе переменной (`number`) и другая, которая поможет сжимателю лучше оптимизировать код (`const` -- константа). [/smart] ## Оптимизации Сжиматель бегает по дереву, ищет "паттерны" -- известные ему структуры, которые он знает, как оптимизировать, и обновляет дерево. В разных минификаторах реализован разный набор оптимизаций, сами оптимизации применяются в разном порядке, поэтому результаты работы могут отличаться. В примерах ниже даётся результат работы GCC.
Объединение и сжатие констант
До оптимизации: ```js function test(a, b) { run(a, 'my' + 'string', 600 * 600 * 5, 1 && 0, b && 0) } ``` После: ```js //+ no-beautify function test(a,b){run(a,"mystring",18E5,0,b&&0)}; ```
Укорачивание локальных переменных
До оптимизации: ```js function sayHi(*!*name*/!*, *!*message*/!*) { alert(name +" сказал: " + message); } ``` После оптимизации: ```js //+ no-beautify function sayHi(a,b){alert(a+" сказал: "+b)}; ```
Объединение и удаление локальных переменных
До оптимизации: ```js function test(nodeId) { var elem = document.getElementsById(nodeId); var parent = elem.parentNode; alert( parent ); } ``` После оптимизации GCC: ```js //+ no-beautify function test(a){a=document.getElementsById(a).parentNode;alert(a)}; ```
Уничтожение недостижимого кода, разворачивание `if`-веток
До оптимизации: ```js function test(node) { var parent = node.parentNode; if (0) { alert( "Привет с параллельной планеты" ); } else { alert( "Останется только один" ); } return; alert( 1 ); } ``` После оптимизации: ```js //+ no-beautify function test(){alert("Останется только один")} ```
Переписывание синтаксических конструкций
До оптимизации: ```js var i = 0; while (i++ < 10) { alert( i ); } if (i) { alert( i ); } if (i == '1') { alert( 1 ); } else if (i == '2') { alert( 2 ); } else { alert( i ); } ``` После оптимизации: ```js //+ no-beautify for(var i=0;10>i++;)alert(i);i&&alert(i);"1"==i?alert(1):"2"==i?alert(2):alert(i); ```
Инлайнинг функций
*Инлайнинг функции* -- приём оптимизации, при котором функция заменяется на своё тело. До оптимизации: ```js function sayHi(message) { var elem = createMessage('div', message); showElement(elem); function createMessage(tagName, message) { var el = document.createElement(tagName); el.innerHTML = message; return el; } function showElement(elem) { document.body.appendChild(elem); } } ``` После оптимизации (переводы строк также будут убраны): ```js function sayHi(b) { var a = document.createElement("div"); a.innerHTML = b; document.body.appendChild(a) }; ```
Инлайнинг переменных
Переменные заменяются на значение, если оно заведомо известно. До оптимизации: ```js (function() { var isVisible = true; var hi = "Привет вам из JavaScript"; window.sayHi = function() { if (isVisible) { alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); alert( hi ); } } })(); ``` После оптимизации: ```js (function() { window.sayHi = function() { alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); alert( "Привет вам из JavaScript" ); }; } })(); ``` Казалось бы -- зачем менять `hi` на строку? Ведь код стал ощутимо длиннее! ...Но всё дело в том, что минификатор знает, что дальше код будет сжиматься при помощи gzip. Во всяком случае, все правильно настроенные сервера так делают. [Алгоритм работы gzip](http://www.gzip.org/algorithm.txt) заключается в том, что он ищет повторы в данных и выносит их в специальный "словарь", заменяя на более короткий идентификатор. Архив как раз и состоит из словаря и данных, в которых дубликаты заменены на идентификаторы. Если вынести строку обратно в переменную, то получится как раз частный случай такого сжатия -- взяли `"Привет вам из JavaScript"` и заменили на идентификатор `hi`. Но gzip справляется с этим лучше, поэтому эффективнее будет оставить именно строку. Gzip сам найдёт дубликаты и сожмёт их. Плюс такого подхода станет очевиден, если сжать gzip оба кода -- до и после минификации. Минифицированный gzip-сжатый код в итоге даст меньший размер.
Разные мелкие оптимизации
Кроме основных оптимизаций, описанных выше, есть ещё много мелких:
## Подводные камни Описанные оптимизации, в целом, безопасны, но есть ряд подводных камней. ### Конструкция with Рассмотрим код: ```js //+ no-beautify function changePosition(style) { var position, test; *!* with (style) { position = 'absolute'; } */!* } ``` Куда будет присвоено значение `position = 'absolute'`? Это неизвестно до момента выполнения: если свойство `position` есть в `style` -- то туда, а если нет -- то в локальную переменную. Можно ли в такой ситуации заменить локальную переменную на более короткую? Очевидно, нет: ```js //+ no-beautify function changePosition(style) { var a, b; *!* with (style) { // а что, если в style нет такого свойства? position = 'absolute';// куда будет осуществлена запись? в window.position? } */!* } ``` Такая же опасность для сжатия кроется в использованном `eval`. Ведь `eval` может обращаться к локальным переменным: ```js //+ no-beautify function f(code) { var myVar; eval(code); // а что, если будет присвоение eval("myVar = ...") ? alert(myVar); ``` Получается, что при наличии `eval` мы не имеем права переименовывать локальные переменные. Причём (!), если функция является вложенноой, то и во внешних функциях тоже. А ведь сжатие переменных -- очень важная оптимизация. Как правило, она уменьшает размер сильнее всего. Что делать? Разные минификаторы поступают по-разному. Ни тот ни другой вариант нас, по большому счёту, не устраивают. **Для того, чтобы код сжимался хорошо и работал правильно, не используем `with` и `eval`.** Либо, если уж очень надо использовать -- делаем это с оглядкой на поведение минификатора, чтобы не было проблем. ### Условная компиляция IE10- В IE10- поддерживалось [условное выполнение JavaScript](http://msdn.microsoft.com/en-us/library/121hztk3.aspx). Синтаксис: `/*@cc_on код */`. Такой код выполнится в IE10-, например: ```js //+ run var isIE /*@cc_on =true@*/ ; alert( isIE ); // true в IE10- ``` Можно хитро сделать, чтобы комментарий остался, например так: ```js //+ run var isIE = new Function('', '/*@cc_on return true@*/')(); alert( isIE ); // true в IE. ``` ...Однако, с учётом того, что в современных IE11+ эта компиляция не работает в любом случае, лучше избавиться от неё вообще. В следующих главах мы посмотрим, какие продвинутые возможности есть в минификаторах, как сделать сжатие более эффективным.