530 lines
26 KiB
Markdown
530 lines
26 KiB
Markdown
# 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 <файл экстернов.js></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>
|