513 lines
21 KiB
Markdown
513 lines
21 KiB
Markdown
# Как работают сжиматели JavaScript
|
||
|
||
Перед выкладыванием JavaScript на "боевую" машину -- пропускаем его через минификатор (также говорят "сжиматель"), который удаляет пробелы и по-всякому оптимизирует код, уменьшая его размер.
|
||
|
||
В этой статье мы посмотрим, как работают современные минификаторы, за счёт чего они укорачивают код и какие с ними возможны проблемы.
|
||
[cut]
|
||
|
||
## Современные сжиматели
|
||
|
||
Рассматриваемые в этой статье алгоритмы и подходы относятся к минификаторам последнего поколения.
|
||
|
||
Вот их список:
|
||
|
||
<ul>
|
||
<li>[Google Closure Compiler](https://developers.google.com/closure/compiler/) </li>
|
||
<li>[UglifyJS](https://github.com/mishoo/UglifyJS)</li>
|
||
<li>[Microsoft AJAX Minifier](http://ajaxmin.codeplex.com/)</li>
|
||
</ul>
|
||
Самые широко используемые -- первые два, поэтому будем рассматривать в первую очередь их.
|
||
|
||
Наша цель -- понять, как они работают, и что интересного с их помощью можно сотворить.
|
||
|
||
## С чего начать?
|
||
|
||
Для GCC:
|
||
|
||
<ol>
|
||
<li>Убедиться, что стоит [Java](http://java.oracle.com)</li>
|
||
<li>Скачать и распаковать [](http://closure-compiler.googlecode.com/files/compiler-latest.zip), нам нужен файл `compiler.jar`.</li>
|
||
<li>Сжать файл `my.js`: `java -jar compiler.jar --charset UTF-8 --js my.js --js_output_file my.min.js`</li>
|
||
</ol>
|
||
|
||
Обратите внимание на флаг `--charset` для GCC. Без него русские буквы будут закодированы во что-то типа `\u1234`.
|
||
|
||
Google Closure Compiler также содержит [песочницу](http://closure-compiler.appspot.com/home) для тестирования сжатия и [веб-сервис](https://developers.google.com/closure/compiler/docs/gettingstarted_api?hl=ru), на который код можно отправлять для сжатия. Но скачать файл обычно гораздо проще, поэтому его редко где используют.
|
||
|
||
Для UglifyJS:
|
||
|
||
<ol>
|
||
<li>Убедиться, что стоит [Node.js](http://nodejs.org)</li>
|
||
<li>Поставить `npm install -g uglify-js`.</li>
|
||
<li>Сжать файл `my.js`: `uglifyjs my.js -o my.min.js`</li>
|
||
</ol>
|
||
|
||
## Что делает минификатор?
|
||
|
||
Все современные минификаторы работают следующим образом:
|
||
<ol>
|
||
<li>Разбирают JavaScript-код в синтаксическое дерево.
|
||
|
||
Также поступает любой интерпретатор JavaScript перед тем, как его выполнять. Но затем, вместо исполнения кода...</li>
|
||
<li>Бегают по этому дереву, анализируют и оптимизируют его.</li>
|
||
<li>Записывают из синтаксического дерева получившийся код.</li>
|
||
</ol>
|
||
|
||
## Как выглядит дерево?
|
||
|
||
Посмотреть синтаксическое дерево можно, запустив компилятор со специальным флагом.
|
||
|
||
Для GCC есть даже способ вывести его:
|
||
|
||
<ol>
|
||
<li>Сначала сгенерируем дерево в формате [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` убирает лишнюю служебную информацию.
|
||
</li>
|
||
<li>Файл в этом формате используется в различных программах для графопостроения.
|
||
|
||
Чтобы превратить его в обычную картинку, подойдёт утилита `dot` из пакета [Graphviz](http://www.graphviz.org/):
|
||
|
||
```
|
||
// конвертировать в формат png
|
||
dot -Tpng my.dot -o my.png
|
||
|
||
// конвертировать в формат svg
|
||
dot -Tsvg my.dot -o my.svg
|
||
```
|
||
|
||
</li>
|
||
</ol>
|
||
|
||
Пример кода `my.js`:
|
||
|
||
```js
|
||
function User(name) {
|
||
|
||
this.sayHi = function() {
|
||
alert( name );
|
||
};
|
||
|
||
}
|
||
```
|
||
|
||
Результат, получившееся из `my.js` дерево:
|
||
|
||
<img src="my.png">
|
||
|
||
В узлах-эллипсах на иллюстрации выше стоит тип, например `FUNCTION` (функция) или `NAME` (имя переменной). Комментарии к ним на русском языке добавлены мной вручную.
|
||
|
||
Кроме него к каждому узлу привязаны конкретные данные. Сжиматель умеет ходить по этому дереву и менять его, как пожелает.
|
||
|
||
[smart header="Комментарии JSDoc"]
|
||
Обычно когда код превращается в дерево -- из него естественным образом исчезают комментарии и пробелы. Они не имеют значения при выполнении, поэтому игнорируются.
|
||
|
||
Но Google Closure Compiler добавляет в дерево информацию из *комментариев JSDoc*, т.е. комментариев вида `/** ... */`, например:
|
||
|
||
```js
|
||
*!*
|
||
/**
|
||
* Номер минимальной поддерживаемой версии IE
|
||
* @const
|
||
* @type {number}
|
||
*/
|
||
*/!*
|
||
var minIEVersion = 8;
|
||
```
|
||
|
||
Такие комментарии не создают новых узлов дерева, а добавляются в качестве информации к существующем. В данном случае -- к переменной `minIEVersion`.
|
||
|
||
В них может содержаться информация о типе переменной (`number`) и другая, которая поможет сжимателю лучше оптимизировать код (`const` -- константа).
|
||
|
||
[/smart]
|
||
|
||
## Оптимизации
|
||
|
||
Сжиматель бегает по дереву, ищет "паттерны" -- известные ему структуры, которые он знает, как оптимизировать, и обновляет дерево.
|
||
|
||
В разных минификаторах реализован разный набор оптимизаций, сами оптимизации применяются в разном порядке, поэтому результаты работы могут отличаться. В примерах ниже даётся результат работы GCC.
|
||
|
||
|
||
<dl>
|
||
<dt>Объединение и сжатие констант</dt>
|
||
<dd>
|
||
До оптимизации:
|
||
|
||
```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)};
|
||
```
|
||
|
||
<ul>
|
||
<li>`'my' + 'string'` -> `"mystring"`.</li>
|
||
<li>`600 * 600 * 5` -> `18E5` (научная форма числа, для краткости).</li>
|
||
<li>`1 && 0` -> `0`.</li>
|
||
<li>`b && 0` -> без изменений, т.к. результат зависит от `b`.</li>
|
||
</ul>
|
||
</dd>
|
||
<dt>Укорачивание локальных переменных</dt>
|
||
<dd>
|
||
До оптимизации:
|
||
|
||
```js
|
||
function sayHi(*!*name*/!*, *!*message*/!*) {
|
||
alert(name +" сказал: " + message);
|
||
}
|
||
```
|
||
|
||
После оптимизации:
|
||
|
||
```js
|
||
//+ no-beautify
|
||
function sayHi(a,b){alert(a+" сказал: "+b)};
|
||
```
|
||
|
||
<ul>
|
||
<li>Локальная переменная заведомо доступна только внутри функции, поэтому обычно её переименование безопасно (необычные случаи рассмотрим далее).</li>
|
||
<li>Также переименовываются локальные функции.</li>
|
||
<li>Вложенные функции обрабатываются корректно.</li>
|
||
</ul>
|
||
</dd>
|
||
<dt>Объединение и удаление локальных переменных</dt>
|
||
<dd>
|
||
До оптимизации:
|
||
|
||
```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)};
|
||
```
|
||
|
||
<ul>
|
||
<li>Локальные переменные были переименованы. </li>
|
||
<li>Лишние переменные убраны. Для этого сжиматель создаёт вспомогательную внутреннюю структуру данных, в которой хранятся сведения о "пути использования" каждой переменной. Если одна переменная заканчивает свой путь и начинает другая, то вполне можно дать им одно имя.</li>
|
||
<li>Кроме того, операции `elem = getElementsById` и `elem.parentNode` объединены, но это уже другая оптимизация.</li>
|
||
</ul>
|
||
</dd>
|
||
<dt>Уничтожение недостижимого кода, разворачивание `if`-веток</dt>
|
||
<dd>
|
||
|
||
До оптимизации:
|
||
|
||
```js
|
||
function test(node) {
|
||
var parent = node.parentNode;
|
||
|
||
if (0) {
|
||
alert( "Привет с параллельной планеты" );
|
||
} else {
|
||
alert( "Останется только один" );
|
||
}
|
||
|
||
return;
|
||
|
||
alert( 1 );
|
||
}
|
||
```
|
||
|
||
После оптимизации:
|
||
|
||
```js
|
||
//+ no-beautify
|
||
function test(){alert("Останется только один")}
|
||
```
|
||
|
||
<ul>
|
||
<li>Если переменная присваивается, но не используется, она может быть удалена. В примере выше эта оптимизация была применена к переменной `parent`, а затем и к параметру `node`.</li>
|
||
<li>Заведомо ложная ветка `if(0) { .. }` убрана, заведомо истинная -- оставлена.
|
||
|
||
То же самое будет с условиями в других конструкциях, например `a = true ? c : d` превратится в `a = c`.</li>
|
||
<li>Код после `return` удалён как недостижимый.</li>
|
||
</ul>
|
||
</dd>
|
||
<dt>Переписывание синтаксических конструкций</dt>
|
||
<dd>
|
||
До оптимизации:
|
||
|
||
```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);
|
||
```
|
||
|
||
<ul>
|
||
<li>Конструкция `while` переписана в `for`.</li>
|
||
<li>Конструкция `if (i) ...` переписана в `i&&...`.</li>
|
||
<li>Конструкция `if (cond) ... else ...` была переписана в `cond ? ... : ...`.</li>
|
||
</ul>
|
||
</dd>
|
||
<dt>Инлайнинг функций</dt>
|
||
<dd>
|
||
*Инлайнинг функции* -- приём оптимизации, при котором функция заменяется на своё тело.
|
||
|
||
До оптимизации:
|
||
|
||
```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)
|
||
};
|
||
```
|
||
|
||
<ul>
|
||
<li>Вызовы функций `createMessage` и `showElement` заменены на тело функций. В данном случае это возможно, так как функции используются всего по разу.</li>
|
||
<li>Эта оптимизация применяется не всегда. Если бы каждая функция использовалась много раз, то с точки зрения размера выгоднее оставить их "как есть".</li>
|
||
</ul>
|
||
</dd>
|
||
<dt>Инлайнинг переменных</dt>
|
||
<dd>Переменные заменяются на значение, если оно заведомо известно.
|
||
|
||
До оптимизации:
|
||
|
||
```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" );
|
||
};
|
||
}
|
||
})();
|
||
```
|
||
|
||
<ul>
|
||
<li>Переменная `isVisible` заменена на `true`, после чего `if` стало возможным убрать.</li>
|
||
<li>Переменная `hi` заменена на строку.</li>
|
||
</ul>
|
||
|
||
Казалось бы -- зачем менять `hi` на строку? Ведь код стал ощутимо длиннее!
|
||
|
||
...Но всё дело в том, что минификатор знает, что дальше код будет сжиматься при помощи gzip. Во всяком случае, все правильно настроенные сервера так делают.
|
||
|
||
[Алгоритм работы gzip](http://www.gzip.org/algorithm.txt) заключается в том, что он ищет повторы в данных и выносит их в специальный "словарь", заменяя на более короткий идентификатор. Архив как раз и состоит из словаря и данных, в которых дубликаты заменены на идентификаторы.
|
||
|
||
Если вынести строку обратно в переменную, то получится как раз частный случай такого сжатия -- взяли `"Привет вам из JavaScript"` и заменили на идентификатор `hi`. Но gzip справляется с этим лучше, поэтому эффективнее будет оставить именно строку. Gzip сам найдёт дубликаты и сожмёт их.
|
||
|
||
Плюс такого подхода станет очевиден, если сжать gzip оба кода -- до и после минификации. Минифицированный gzip-сжатый код в итоге даст меньший размер.
|
||
</dd>
|
||
|
||
<dt>Разные мелкие оптимизации</dt>
|
||
<dd>Кроме основных оптимизаций, описанных выше, есть ещё много мелких:
|
||
|
||
<ul>
|
||
<li>Убираются лишние кавычки у ключей
|
||
|
||
```js
|
||
//+ no-beautify
|
||
{"prop" : "val" } => {prop:"val"}
|
||
```
|
||
|
||
</li>
|
||
<li>Упрощаются простые вызовы `Array/Object`
|
||
|
||
```js
|
||
//+ no-beautify
|
||
a = new Array() => a = []
|
||
o = new Object() => o = {}
|
||
```
|
||
|
||
Эта оптимизация предполагает, что `Array` и `Object` не переопределены программистом. Для включения её в UglifyJS нужен флаг `--unsafe`.
|
||
</li>
|
||
<li>...И еще некоторые другие мелкие изменения кода...</li>
|
||
</ul>
|
||
|
||
|
||
</dd>
|
||
</dl>
|
||
|
||
## Подводные камни
|
||
|
||
Описанные оптимизации, в целом, безопасны, но есть ряд подводных камней.
|
||
|
||
### Конструкция 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` мы не имеем права переименовывать локальные переменные. Причём (!), если функция является вложенноой, то и во внешних функциях тоже.
|
||
|
||
А ведь сжатие переменных -- очень важная оптимизация. Как правило, она уменьшает размер сильнее всего.
|
||
|
||
Что делать? Разные минификаторы поступают по-разному.
|
||
|
||
<ul>
|
||
<li>UglifyJS -- не будет переименовывать переменные. Так что наличие `with/eval` сильно повлияет на степень сжатие кода.</li>
|
||
<li>GCC -- всё равно сожмёт локальные переменные. Это, конечно же, может привести к ошибкам, причём в сжатом коде, отлаживать который не очень-то удобно. Поэтому он выдаст предупреждение о наличии опасной конструкции.</li>
|
||
</ul>
|
||
|
||
Ни тот ни другой вариант нас, по большому счёту, не устраивают.
|
||
|
||
**Для того, чтобы код сжимался хорошо и работал правильно, не используем `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+ эта компиляция не работает в любом случае, лучше избавиться от неё вообще.
|
||
|
||
В следующих главах мы посмотрим, какие продвинутые возможности есть в минификаторах, как сделать сжатие более эффективным.
|
||
|