renovations

This commit is contained in:
Ilya Kantor 2015-04-10 17:42:53 +03:00
parent 631be9c6ad
commit 051b6b43c3
30 changed files with 2603 additions and 0 deletions

View file

@ -0,0 +1,530 @@
# GCC: продвинутые оптимизации
Продвинутый режим оптимизации google closure compiler включается опцией <code>--compilation_level ADVANCED_OPTIMIZATIONS.</code>
Слово "продвинутый" (advanced) здесь, пожалуй, не совсем подходит. Было бы более правильно назвать его "супер-агрессивный-ломающий-ваш-неподготовленный-код-режим". Кардинальное отличие применяемых оптимизаций от обычных (simple) -- в том, что они небезопасны.
Чтобы им пользоваться -- надо уметь это делать.
[cut]
## Основной принцип продвинутого режима
<ul>
<li>Если в обычном режиме переименовываются только локальные переменные внутри функций, то в "продвинутом" -- на более короткие имена заменяется все.</li>
<li>Если в обычном режиме удаляется недостижимый код после <code>return</code>, то в продвинутом -- вообще весь код, который не вызывается в явном виде.</li>
</ul>
Например, если запустить продвинутую оптимизацию на таком коде:
```js
// my.js
function test(node) {
node.innerHTML = "newValue"
}
```
Строка запуска компилятора:
```
java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js my.js
```
...То результат будет -- пустой файл. Google Closure Compiler увидит, что функция <code>test</code> не используется, и с чистой совестью вырежет ее.
А в следующем скрипте функция сохранится:
```js
function test(n) {
alert( "this is my test number " + n );
}
test(1);
test(2);
```
После сжатия:
```js
function a(b) {
alert("this is my test number " + b)
}
a(1);
a(2);
```
Здесь в скрипте присутствует явный вызов функции, поэтому она сохранилась.
Конечно, есть способы, чтобы сохранить функции, вызов которых происходит вне скрипта, и мы их обязательно рассмотрим.
**Продвинутый режим сжатия не предусматривает сохранения глобальных переменных. Он переименовывает, инлайнит, удаляет вообще все символы, кроме зарезервированных.**
Иначе говоря, продвинутый режим (ADVANCED_OPTIMIZATIONS), в отличие от простого (SIMPLE_OPTIMIZATIONS -- по умолчанию), вообще не заботится о доступности кода извне и сохранении ссылочной целостности относительно внешних скриптов.
Единственное, что он гарантирует -- это внутреннюю ссылочную целостность, и то -- при соблюдении ряда условий и практик программирования.
Собственно, за счет такого агрессивного подхода и достигается дополнительный эффект оптимизации и сжатия скриптов.
[summary]
То есть, продвинутый режим - это не просто "улучшенный обычный", а принципиально другой, небезопасный и обфусцирующий подход к сжатию.
Этот режим является "фирменной фишкой" Google Closure Compiler, недоступной при использовании других компиляторов.
[/summary]
Для того, чтобы эффективно сжимать Google Closure Compiler в продвинутом режиме, нужно понимать, что и как он делает. Это мы сейчас обсудим.
### Сохранение ссылочной целостности
Чтобы использовать сжатый скрипт, мы должны иметь возможность вызывать функции под теми именами, которые им дали.
То есть, перед нами стоит задача *сохранения ссылочной целостности*, которая заключается в том, чтобы обеспечить доступность нужных функций для обращений по исходному имени извне скрипта.
Существует два способа сохранения внешней ссылочной целостности: экстерны и экспорты. Мы в подробностях рассмотрим оба, но перед этим необходимо упомянуть о модулях -- другой важнейшей возможности GCC.
### Модули
При сжатии GCC можно указать одновременно много JavaScript-файлов. "Эка невидаль, " -- скажете вы, и будете правы. Да, пока что ничего особого.
Но в дополнение к этому можно явно указать, какие исходные файлы сжать в какие файлы результата. То есть, разбить итоговую сборку на модули.
Так что страницы могут грузить модули по мере надобности. Например, по умолчанию -- главный, а дополнительная функциональность -- загружаться лишь там, где она нужна.
Для такой сборки используется флаг компилятора `--module имя:количество файлов`.
Например:
```
java -jar compiler.jar --js base.js --js main.js --js admin.js --module
first:2 --module second:1:first
```
Эта команда создаст модули: first.js и second.js.
Первый модуль, который назван "first", создан из объединённого и оптимизированного кода первых двух файлов (`base.js` и `main.js`).
Второй модуль, который назван "second", создан из `admin.js` -- это следующий аргумент `--js` после включенных в первый модуль.
Второй модуль в нашем случае зависит от первого. Флаг `--module second:1:first` как раз означает, что модуль `second` будет создан из одного файла после вошедших в предыдущий модуль (`first`) и зависит от модуля `first`.
А теперь -- самое вкусное.
**Ссылочная целостность между всеми получившимися файлами гарантируется.**
Если в одном функция `doFoo` заменена на `b`, то и в другом тоже будет использоваться `b`.
Это означает, что проблем между JS-файлами не будет. Они могут свободно вызывать друг друга без экспорта, пока находятся в единой модульной сборке.
### Экстерны
Экстерн (extern) -- имя, которое числится в специальном списке компилятора. Он должен быть определен вне скрипта, в файле экстернов.
**Компилятор никогда не переименовывает экстерны.**
Например:
```js
document.onkeyup = function(event) {
alert(event.type)
}
```
После продвинутого сжатия:
```js
document.onkeyup = function(a) {
alert(a.type)
}
```
Как видите, переименованной оказалась только переменная `event`. Такое переименование заведомо безопасно, т.к. `event` -- локальная переменная.
Почему компилятор не тронул остального? Попробуем другой вариант:
```js
document.blabla = function(event) {
alert(event.megaProperty)
}
```
После компиляции:
```js
document.a = function(a) {
alert(a.b)
}
```
Теперь компилятор переименовал и <code>blabla</code> и <code>megaProperty</code>.
Дело в том, что названия, использованные до этого, были во внутреннем списке экстернов компилятора. Этот список охватывает основные объекты браузеров и находится (под именем <code>externs.zip</code>) в корне архива <code>compiler.jar</code>.
**Компилятор переименовывает имя списка экстернов только когда так названа локальная переменная.**
Например:
```js
window.resetNode = function(node) {
var innerHTML = "test";
node.innerHTML = innerHTML;
}
```
На выходе:
```js
window.a = function(a) {
a.innerHTML = "test"
};
```
Как видите, внутренняя переменная <code>innerHTML</code> не просто переименована - она заинлайнена (заменена на значение). Так как переменная локальна, то любые действия внутри функции с ней безопасны.
А свойство <code>innerHTML</code> не тронуто, как и объект <code>window</code> -- так как они в списке экстернов и не являются локальными переменными.
Это приводит к следующему побочному эффекту. Иногда свойства, которые следовало бы сжать, не сжимаются. Например:
```js
window['User'] = function(name, type, age) {
this.name = name
this.type = type
this.age = age
}
```
После сжатия:
```js
window.User = function(a, b, c) {
this.name = a;
this.type = b;
this.a = c
};
```
Как видно, свойство <code>age</code> сжалось, а <code>name</code> и <code>type</code> -- нет. Это побочный эффект экстернов: <code>name</code> и <code>type</code> -- в списке объектов браузера, и компилятор просто старается не наломать дров.
Поэтому отметим еще одно полезное правило оптимизации:
**Названия своих свойств не должны совпадать с зарезервированными словами (экстернами). Тогда они будут хорошо сжиматься.**
Для задания списка экстернов их достаточно перечислить в файле и указать этот файл флагом <code>--externs &lt;файл экстернов.js&gt;</code>.
При перечислении объектов в файле экстернов - объявляйте их и перечисляйте свойства. Все эти объявления никуда не идут, они используются только для создания списка, который обрабатывается компилятором.
Например, файл `myexterns.js`:
```js
var dojo = {}
dojo._scopeMap;
```
Использование такого файла при сжатии (опция <code>--externs myexterns.js</code>) приведет к тому, что все обращения к символам <code>dojo</code> и к <code>dojo._scopeMap</code> будут не сжаты, а оставлены "как есть".
### Экспорт
*Экспорт* -- программный ход, основанный на следующем правиле поведения компилятора.
**Компилятор заменяет обращения к свойствам через кавычки на точку, и при этом не трогает название свойства.**
Например, <code>window['User']</code> превратится в <code>window.User</code>, но не дальше.
Таким образом можно *"экспортировать"* нужные функции и объекты:
```js
function SayWidget(elem) {
this.elem = elem
this.init()
}
window['SayWidget'] = SayWidget;
```
На выходе:
```js
function a(b) {
this.a = b;
this.b()
}
window.SayWidget = a;
```
Обратим внимание -- сама функция <code>SayWidget</code> была переименована в <code>a</code>. Но затем -- экспортирована как <code>window.SayWidget</code>, и таким образом доступна внешним скриптам.
Добавим пару методов в прототип:
```js
function SayWidget(elem) {
this.elem = elem;
this.init();
}
SayWidget.prototype = {
init: function() {
this.elem.style.display = 'none'
},
setSayHandler: function() {
this.elem.onclick = function() {
alert("hi")
};
}
}
window['SayWidget'] = SayWidget;
SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler;
```
После сжатия:
```js
//+ no-beautify
function a(b) {
this.a = b;
this.b()
}
a.prototype = {b:function() {
this.a.style.display = "none"
}, c:function() {
this.a.onclick = function() {
alert("hi")
}
}};
window.SayWidget = a;
a.prototype.setSayHandler = a.prototype.c;
```
Благодаря строке
```js
SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
```
метод <code>setSayHandler</code> экспортирован и доступен для внешнего вызова.
Сама строка экспорта выглядит довольно глупо. По виду -- присваиваем свойство самому себе.
Но логика сжатия GCC работает так, что такая конструкция является экспортом. Справа переименование свойства <code>setSayHandler</code> происходит, а слева -- нет.
[smart header="Планируйте жизнь после сжатия"]
Рассмотрим следующий код:
```js
window['Animal'] = function() {
this.blabla = 1;
this['blabla'] = 2;
}
```
После сжатия:
```js
window.Animal = function() {
this.a = 1;
this.blabla = 2
};
```
Как видно, первое обращение к свойству <code>blabla</code> сжалось, а второе (как и все аналогичные) -- преобразовалось в синтаксис через точку.
В результате получили некорректное поведение кода.
Так что, используя продвинутый режим оптимизации, планируйте поведение кода после сжатия.
**Если где-то возможно обращение к свойствам через квадратные скобки по полному имени -- такое свойство должно быть экспортировано.**
[/smart]
### goog.exportSymbol и goog.exportProperty
В библиотеке [Google Closure Library](https://developers.google.com/closure/library/) для экспорта есть специальная функция <code>goog.exportSymbol</code>. Вызывается так:
```js
goog.exportSymbol('my.SayWidget', SayWidget)
```
Эта функция по сути работает также, как и рассмотренная выше строка с присвоением свойства, но при необходимости создает нужные объекты.
Она аналогична коду:
```js
window['my'] = window['my'] || {}
window['my']['SayWidget'] = SayWidget
```
То есть, если путь к объекту не существует -- <code>exportSymbol</code> создаст нужные пустые объекты.
Функция <code>goog.exportProperty</code> экспортирует свойство объекта:
```js
goog.exportProperty(SayWidget.prototype, 'setSayHandler', SayWidget.prototype.setSayHandler)
```
Строка выше - то же самое, что и:
```js
SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
```
Зачем они нужны, если все можно сделать простым присваиванием?
Основная цель этих функций -- во взаимодействии с Google Closure Compiler. Они дают информацию компилятору об экспортах, которую он может использовать.
Например, есть недокументированная внутренняя опция <code>externExportsPath</code>, которая генерирует из всех экспортов файл экстернов. Таким образом можно распространять откомпилированный JavaScript-файл как внешнюю библиотеку, с файлом экстернов для удобного внешнего связывания.
Кроме того, экспорт через эти функциями удобен и нагляден.
Если вы используете продвинутый режим оптимизации, то можно взять их из файла base.js Google Closure Library. Можно и подключить этот файл целиком -- оптимизатор при продвинутом сжатии вырежет из него почти всё лишнее, так что overhead будет минимальным.
### Отличия экспорта от экстерна
Между экспортом и экстерном есть кое-что общее. И то и другое дает возможность доступа к объектам под исходным именем, до переименования.
Но, в остальном, это совершенно разные вещи.
<table>
<thead>
<tr>
<th>Экстерн</th>
<th>Экспорт</th>
</tr>
</thead>
<tbody>
<tr>
<td>Служит для тотального запрета на переименование всех обращений к свойству.
Задумано для сохранения обращений к стандартным объектам браузера, внешним библиотекам.</td>
<td>Служит для открытия доступа к свойству извне под указанным именем.
Задумано для открытия внешнего интерфейса к сжатому скрипту.</td>
</tr>
<tr>
<td>Работает со свойством, объявленным вне скрипта.
Вы не можете объявить новое свойство в скрипте и сделать его экстерном.</td>
<td>Создает ссылку на свойство, объявленное в скрипте.</td>
</tr>
<tr>
<td>Если <code>window</code> - экстерн, то все обращения к <code>window</code> в скрипте останутся как есть.</td>
<td>Если <code>user</code> экспортируется, то создается только одна ссылка под полным именем, а все остальные обращения будут сокращены.</td>
</tr>
</tbody>
</table>
## Стиль разработки
Посмотрим, как сжиматель поведёт себя на следующем, типичном, объявлении библиотеки:
```js
(function(window, undefined) {
// пространство имен и локальная перменная для него
var MyFramework = window.MyFramework = {};
// функция фреймворка, доступная снаружи
MyFramework.publicOne = function() {
makeElem();
};
// приватная функция фреймворка
function makeElem() {
var div = document.createElement('div');
document.body.appendChild(div);
}
// еще какая-то функция
MyFramework.publicTwo = function() {};
})(window);
// использование
MyFramework.publicOne();
```
Результат компиляции в обычном режиме:
```js
//+ no-beautify
// java -jar compiler.jar --js myframework.js --formatting PRETTY_PRINT
(function(a) {
a = a.MyFramework = {};
a.publicOne = function() {
var a = document.createElement("div");
document.body.appendChild(a)
};
a.publicTwo = function() {
}
})(window);
MyFramework.publicOne();
```
Это -- примерно то, что мы ожидали. Неиспользованный метод `publicTwo` остался, локальные свойства переименованы и заинлайнены.
А теперь продвинутый режим:
```js
// --compilation_level ADVANCED_OPTIMIZATIONS
window.a = {};
MyFramework.b();
```
Оно не работает! Компилятор попросту не разобрался, что и как вызывается, и превратил рабочий JS-файл в один сплошной баг.
В зависимости от версии GCC у вас может быть и что-то другое.
Всё дело в том, что такой стиль объявления нетипичен для инструментов, которые в самом Google разрабатываются и сжимаются этим минификатором.
Типичный правильный стиль:
```js
// пространство имен и локальная перменная для него
var MyFramework = {};
MyFrameWork._makeElem = function() {
var div = document.createElement('div');
document.body.appendChild(div);
};
MyFramework.publicOne = function() {
MyFramework._makeElem();
};
MyFramework.publicTwo = function() {};
// использование
MyFramework.publicOne();
```
Обычное сжатие здесь будет бесполезно, а вот продвинутый режим идеален:
```js
// в зависимости от версии GCC результат может отличаться
MyFrameWork.a = function() {
var a = document.createElement("div");
document.body.appendChild(a)
};
MyFrameWork.a();
```
Google Closure Compiler не только разобрался в структуре и удалил лишний метод - он заинлайнил функции, чтобы итоговый размер получился минимальным.
Как говорится, преимущества налицо.
## Резюме
Продвинутый режим оптимизации сжимает, оптимизирует и, при возможности, удаляет все свойства и методы, за исключением экстернов.
Это является принципиальным отличием, по сравнению с другими упаковщиками.
Отказ от сохранения внешней ссылочной целостности с одной стороны позволяет увеличить уровень сжатия, но требует поддержки со стороны разработчика.
Основная проблема этого сжатия -- усложнение разработки. Добавляется дополнительный уровень возможных проблем: сжатие. Конечно, можно отлаживать и сжатый код, для этого придуманы [Source Maps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/), но клиентская разработка и без того достаточно сложна.
Поэтому его используют редко.
Как правило, есть две причины для использования продвинутого режима:
<ol>
<li>**Обфускация кода.**
Если в коде после обычного сжатия ещё как-то можно разобраться, то после продвинутого -- уже нет. Всё переименовано и заинлайнено. В теории это, конечно, возможно, но "порог входа" в такой код несоизмеримо выше.
Судя по виду скриптов на сайтах, созданных Google, сам Google жмет свои скрипты именно продвинутым режимом оптимизации. И библиотека Google Closure Library тоже рассчитана на него.</li>
<li>**Хорошие сжатие виджетов, счётчиков.**
Небольшой код, который отдаётся наружу, может быть сжат в продвинутом режиме. Так как он небольшой -- все ошибки можно легко исправить, а продвинутый режим гарантирует наилучшее сжатие.</li>
</ol>