# GCC: продвинутые оптимизации
Продвинутый режим оптимизации google closure compiler включается опцией --compilation_level ADVANCED_OPTIMIZATIONS.
Слово "продвинутый" (advanced) здесь, пожалуй, не совсем подходит. Было бы более правильно назвать его "супер-агрессивный-ломающий-ваш-неподготовленный-код-режим". Кардинальное отличие применяемых оптимизаций от обычных (simple) -- в том, что они небезопасны.
Чтобы им пользоваться -- надо уметь это делать.
[cut]
## Основной принцип продвинутого режима
return
, то в продвинутом -- вообще весь код, который не вызывается в явном виде.test
не используется, и с чистой совестью вырежет ее.
А в следующем скрипте функция сохранится:
```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)
}
```
Теперь компилятор переименовал и blabla
и megaProperty
.
Дело в том, что названия, использованные до этого, были во внутреннем списке экстернов компилятора. Этот список охватывает основные объекты браузеров и находится (под именем externs.zip
) в корне архива compiler.jar
.
**Компилятор переименовывает имя списка экстернов только когда так названа локальная переменная.**
Например:
```js
window.resetNode = function(node) {
var innerHTML = "test";
node.innerHTML = innerHTML;
}
```
На выходе:
```js
window.a = function(a) {
a.innerHTML = "test"
};
```
Как видите, внутренняя переменная innerHTML
не просто переименована - она заинлайнена (заменена на значение). Так как переменная локальна, то любые действия внутри функции с ней безопасны.
А свойство innerHTML
не тронуто, как и объект window
-- так как они в списке экстернов и не являются локальными переменными.
Это приводит к следующему побочному эффекту. Иногда свойства, которые следовало бы сжать, не сжимаются. Например:
```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
};
```
Как видно, свойство age
сжалось, а name
и type
-- нет. Это побочный эффект экстернов: name
и type
-- в списке объектов браузера, и компилятор просто старается не наломать дров.
Поэтому отметим еще одно полезное правило оптимизации:
**Названия своих свойств не должны совпадать с зарезервированными словами (экстернами). Тогда они будут хорошо сжиматься.**
Для задания списка экстернов их достаточно перечислить в файле и указать этот файл флагом --externs <файл экстернов.js>
.
При перечислении объектов в файле экстернов - объявляйте их и перечисляйте свойства. Все эти объявления никуда не идут, они используются только для создания списка, который обрабатывается компилятором.
Например, файл `myexterns.js`:
```js
var dojo = {}
dojo._scopeMap;
```
Использование такого файла при сжатии (опция --externs myexterns.js
) приведет к тому, что все обращения к символам dojo
и к dojo._scopeMap
будут не сжаты, а оставлены "как есть".
### Экспорт
*Экспорт* -- программный ход, основанный на следующем правиле поведения компилятора.
**Компилятор заменяет обращения к свойствам через кавычки на точку, и при этом не трогает название свойства.**
Например, window['User']
превратится в window.User
, но не дальше.
Таким образом можно *"экспортировать"* нужные функции и объекты:
```js
function SayWidget(elem) {
this.elem = elem
this.init()
}
window['SayWidget'] = SayWidget;
```
На выходе:
```js
function a(b) {
this.a = b;
this.b()
}
window.SayWidget = a;
```
Обратим внимание -- сама функция SayWidget
была переименована в a
. Но затем -- экспортирована как window.SayWidget
, и таким образом доступна внешним скриптам.
Добавим пару методов в прототип:
```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
```
метод setSayHandler
экспортирован и доступен для внешнего вызова.
Сама строка экспорта выглядит довольно глупо. По виду -- присваиваем свойство самому себе.
Но логика сжатия GCC работает так, что такая конструкция является экспортом. Справа переименование свойства setSayHandler
происходит, а слева -- нет.
[smart header="Планируйте жизнь после сжатия"]
Рассмотрим следующий код:
```js
window['Animal'] = function() {
this.blabla = 1;
this['blabla'] = 2;
}
```
После сжатия:
```js
window.Animal = function() {
this.a = 1;
this.blabla = 2
};
```
Как видно, первое обращение к свойству blabla
сжалось, а второе (как и все аналогичные) -- преобразовалось в синтаксис через точку.
В результате получили некорректное поведение кода.
Так что, используя продвинутый режим оптимизации, планируйте поведение кода после сжатия.
**Если где-то возможно обращение к свойствам через квадратные скобки по полному имени -- такое свойство должно быть экспортировано.**
[/smart]
### goog.exportSymbol и goog.exportProperty
В библиотеке [Google Closure Library](https://developers.google.com/closure/library/) для экспорта есть специальная функция goog.exportSymbol
. Вызывается так:
```js
goog.exportSymbol('my.SayWidget', SayWidget)
```
Эта функция по сути работает также, как и рассмотренная выше строка с присвоением свойства, но при необходимости создает нужные объекты.
Она аналогична коду:
```js
window['my'] = window['my'] || {}
window['my']['SayWidget'] = SayWidget
```
То есть, если путь к объекту не существует -- exportSymbol
создаст нужные пустые объекты.
Функция goog.exportProperty
экспортирует свойство объекта:
```js
goog.exportProperty(SayWidget.prototype, 'setSayHandler', SayWidget.prototype.setSayHandler)
```
Строка выше - то же самое, что и:
```js
SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
```
Зачем они нужны, если все можно сделать простым присваиванием?
Основная цель этих функций -- во взаимодействии с Google Closure Compiler. Они дают информацию компилятору об экспортах, которую он может использовать.
Например, есть недокументированная внутренняя опция externExportsPath
, которая генерирует из всех экспортов файл экстернов. Таким образом можно распространять откомпилированный JavaScript-файл как внешнюю библиотеку, с файлом экстернов для удобного внешнего связывания.
Кроме того, экспорт через эти функциями удобен и нагляден.
Если вы используете продвинутый режим оптимизации, то можно взять их из файла base.js Google Closure Library. Можно и подключить этот файл целиком -- оптимизатор при продвинутом сжатии вырежет из него почти всё лишнее, так что overhead будет минимальным.
### Отличия экспорта от экстерна
Между экспортом и экстерном есть кое-что общее. И то и другое дает возможность доступа к объектам под исходным именем, до переименования.
Но, в остальном, это совершенно разные вещи.
Экстерн | Экспорт |
---|---|
Служит для тотального запрета на переименование всех обращений к свойству. Задумано для сохранения обращений к стандартным объектам браузера, внешним библиотекам. | Служит для открытия доступа к свойству извне под указанным именем. Задумано для открытия внешнего интерфейса к сжатому скрипту. |
Работает со свойством, объявленным вне скрипта. Вы не можете объявить новое свойство в скрипте и сделать его экстерном. | Создает ссылку на свойство, объявленное в скрипте. |
Если window - экстерн, то все обращения к window в скрипте останутся как есть. |
Если user экспортируется, то создается только одна ссылка под полным именем, а все остальные обращения будут сокращены. |