renovations
This commit is contained in:
parent
631be9c6ad
commit
051b6b43c3
30 changed files with 2603 additions and 0 deletions
530
6-optimize/7-gcc-advanced-optimization/article.md
Normal file
530
6-optimize/7-gcc-advanced-optimization/article.md
Normal 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 <файл экстернов.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>
|
Loading…
Add table
Add a link
Reference in a new issue