# Как работают сжиматели 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 function test(a,b){run(a,"mystring",18E5,0,b&&0)}; ```
Укорачивание локальных переменных
До оптимизации: ```js function sayHi(*!*name*/!*, *!*message*/!*) { alert(name +" сказал: " + message); } ``` После оптимизации: ```js function sayHi(a,b){alert(a+" сказал: "+b)}; ```
Объединение и удаление локальных переменных
До оптимизации: ```js function test(nodeId) { var elem = document.getElementsById(nodeId); var parent = elem.parentNode; alert(parent); } ``` После оптимизации GCC: ```js 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 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 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 function changePosition(style) { var position, test; *!* with (style) { position = 'absolute'; } */!* } ``` Куда будет присвоено значение `position = 'absolute'`? Это неизвестно до момента выполнения: если свойство `position` есть в `style` -- то туда, а если нет -- то в локальную переменную. Можно ли в такой ситуации заменить локальную переменную на более короткую? Очевидно, нет: ```js function changePosition(style) { var a, b; *!* with (style) { // а что, если в style нет такого свойства? position = 'absolute';// куда будет осуществлена запись? в window.position? } */!* } ``` Такая же опасность для сжатия кроется в использованном `eval`. Ведь `eval` может обращаться к локальным переменным: ```js 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 в IE. ``` Там же доступны и дополнительные директивы: `@_jscript_version`, `@if` и т.п., но речь здесь не о том. Для минификаторов этот "условный" комментарий -- всего лишь обычный комментарий. Они его удалят. Получится, что код не поймёт, где же IE. Что делать?
  1. Первое и наиболее корректное решение -- не использовать условную компиляцию.
  2. Второе, если уж очень надо -- применить хак, завернуть его в `eval` или `new Function` (чтобы сжиматель не ругался): ```js //+ run var isIE = new Function('', '/*@cc_on return true@*/')(); alert(isIE); // true в IE. ```
Ещё раз заметим, что в современных IE11+ эта компиляция не работает в любом случае. В следующих главах мы посмотрим, какие продвинутые возможности есть в минификаторах, как сделать сжатие более эффективным.