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,5 @@
# Введение
В отличие от ряда других курсов учебника, этот раздел -- не является курсом как таковым. Связное и грамотное изложение темы требует времени, которое я пока не могу ему уделить. Но, надеюсь, смогу в будущем.
Пока что раздел содержит лишь некоторые статьи-заметки по теме оптимизации, которые, надеюсь, будут вам полезны.

View file

@ -0,0 +1,513 @@
# Как работают сжиматели 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.svg">
В узлах-эллипсах на иллюстрации выше стоит тип, например `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+ эта компиляция не работает в любом случае, лучше избавиться от неё вообще.
В следующих главах мы посмотрим, какие продвинутые возможности есть в минификаторах, как сделать сжатие более эффективным.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,172 @@
# Улучшаем сжатие кода
Здесь мы обсудим разные приёмы, которые используются, чтобы улучшить сжатие кода.
[cut]
## Больше локальных переменных
Например, код jQuery обёрнут в функцию, запускаемую "на месте".
```js
(function(window, undefined) {
// ...
var jQuery = ...
window.jQuery = jQuery; // сделать переменную глобальной
})(window);
```
Переменные `window` и `undefined` стали локальными. Это позволит сжимателю заменить их на более короткие.
## ООП без прототипов
Приватные переменные будут сжаты и заинлайнены.
Например, этот код хорошо сожмётся:
```js
function User(firstName, lastName) {
var fullName = firstName + ' ' + lastName;
this.sayHi = function() {
showMessage(fullName);
}
function showMessage(msg) {
alert( '**' + msg + '**' );
}
}
```
..А этот -- плохо:
```js
function User(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}
User.prototype.sayHi = function() {
this._showMessage(this._fullName);
}
User.prototype._showMessage = function(msg) {
alert( '**' + msg + '**' );
}
```
Сжимаются только локальные переменные, свойства объектов не сжимаются, поэтому эффект от сжатия для второго кода будет совсем небольшим.
При этом, конечно, нужно иметь в виду общий стиль ООП проекта, достоинства и недостатки такого подхода.
## Сжатие под платформу, define
Можно делать разные сборки в зависимости от платформы (мобильная/десктоп) и браузера.
Ведь не секрет, что ряд функций могут быть реализованы по разному, в зависимости от того, поддерживает ли среда выполнения нужную возможность.
### Способ 1: локальная переменная
Проще всего это сделать локальной переменной в модуле:
```js
(function($) {
*!*
/** @const */
var platform = 'IE';
*/!*
// .....
if (platform == 'IE') {
alert( 'IE' );
} else {
alert( 'NON-IE' );
}
})(jQuery);
```
Нужное значение директивы можно вставить при подготовке JavaScript к сжатию.
Сжиматель заинлайнит её и оптимизирует соответствующие IE.
### Способ 2: define
UglifyJS и GCC позволяют задать значение глобальной переменной из командной строки.
В GCC эта возможность доступна лишь в "продвинутом режиме" работы оптимизатора, который мы рассмотрим далее (он редко используется).
Удобнее в этом плане устроен UglifyJS. В нём можно указать флаг `-d SYMBOL[=VALUE]`, который заменит все переменные `SYMBOL` на указанное значение `VALUE`. Если `VALUE` не указано, то оно считается равным `true`.
Флаг не работает, если переменная определена явно.
Например, рассмотрим код:
```js
// my.js
if (isIE) {
alert( "Привет от IE" );
} else {
alert( "Не IE :)" );
}
```
Сжатие вызовом `uglifyjs -d isIE my.js` даст:
```js
alert( "Привет от IE" );
```
..Ну а чтобы код работал в обычном окружении, нужно определить в нём значение переменной по умолчанию. Это обычно делается в каком-то другом файле (на весь проект), так как если объявить `var isIE` в этом, то флаг `-d isIE` не сработает.
Но можно и "хакнуть" сжиматель, объявив переменную так:
```js
// объявит переменную при отсутствии сжатия
// при сжатии не повредит
window.isIE = window.isIE || getBrowserVersion();
```
## Убираем вызовы console.*
Минификатор имеет в своём распоряжении дерево кода и может удалить ненужные вызовы.
Для UglifyJS это делают опции компилятора:
<ul>
<li>`drop_debugger` -- убирает вызовы `debugger`.</li>
<li>`drop_console` -- убирает вызовы `console.*`.</li>
</ul>
Можно написать и дополнительную функцию преобразования, которая убирает другие вызовы, например для `log.*`:
```js
var uglify = require('uglify-js');
var pro = uglify.uglify;
function ast_squeeze_console(ast) {
var w = pro.ast_walker(),
walk = w.walk,
scope;
return w.with_walkers({
"stat": function(stmt) {
if (stmt[0] === "call" && stmt[1][0] == "dot" && stmt[1][1] instanceof Array && stmt[1][1][0] == 'name' && stmt[1][1][1] == "log") {
return ["block"];
}
return ["stat", walk(stmt)];
},
"call": function(expr, args) {
if (expr[0] == "dot" && expr[1] instanceof Array && expr[1][0] == 'name' && expr[1][1] == "console") {
return ["atom", "0"];
}
}
}, function() {
return walk(ast);
});
};
```
Эту функцию следует вызвать на результате `parse`, и она пройдётся по дереву и удалит все вызовы `log.*`.

View file

@ -0,0 +1,317 @@
# Утечки памяти
*Утечки памяти* происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов.
Обычно это происходит автоматически ([](/memory-management)). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях:
[cut]
<ol>
<li>Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.</li>
<li>Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включенным, чтобы не закрывать браузер с кучей вкладок. Приходит утром -- а браузер съел всю память <strike>и рухнул</strike> и сильно тормозит.</li>
</ol>
Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре JavaScript-кода. Мы разберём несколько наиболее частых и важных примеров.
## Коллекция утечек в IE
### Утечка DOM ↔ JS в IE8-
IE до версии 8 не умел очищать циклические ссылки, появляющиеся между DOM-объектами и объектами JavaScript. В результате и DOM и JS оставались в памяти навсегда.
В браузере IE8 была проведена серьёзная работа над ошибками, но утечка в IE8- появляется, если круговая ссылка возникает "через объект".
Чтобы было понятнее, о чём речь, посмотрите на следующий код. Он вызывает утечку памяти в IE8-:
```
function leak() {
// Создаём новый DIV, добавляем к BODY
var elem = document.createElement('div');
document.body.appendChild(elem);
// Записываем в свойство жирный объект
elem.__expando = {
bigAss: new Array(1000000).join('lalala')
};
*!*
// Создаём круговую ссылку. Без этой строки утечки не будет.
elem.__expando.__elem = elem;
*/!*
// Удалить элемент из DOM. Браузер должен очистить память.
elem.parentElement.removeChild(elem);
}
```
Полный пример (только для IE8-, а также IE9 в режиме совместимости с IE8):
[codetabs src="leak-ie8"]
Круговая ссылка и, как следствие, утечка может возникать и неявным образом, через замыкание:
```js
function leak() {
var elem = document.createElement('div');
document.body.appendChild(elem);
elem.__expando = {
bigAss: new Array(1000000).join('lalala'),
*!*
method: function() {} // создаётся круговая ссылка через замыкание
*/!*
};
// Удалить элемент из DOM. Браузер должен очистить память.
elem.parentElement.removeChild(elem);
}
```
Полный пример (IE8-, IE9 в режиме совместимости с IE8):
[codetabs src="leak-ie8-2"]
Без привязки метода `method` к элементу здесь утечки не возникнет.
Бывает ли такая ситуация в реальной жизни? Или это -- целиком синтетический пример, для заумных программистов?
Да, конечно бывает. Например, при разработке графических компонент -- бывает удобно присвоить DOM-элементу ссылку на JavaScript-объект, который представляет собой компонент. Это упрощает делегирование и, в общем-то, логично, что DOM-элемент знает о компоненте на себе. Но в IE8- прямая привязка ведёт к утечке памяти!
Примерно так:
```js
function Menu(elem) {
elem.onclick = function() {};
}
var menu = new Menu(elem); // Menu содержит ссылку на elem
*!*
elem.menu = menu; // такая привязка или что-то подобное ведёт к утечке в IE8
*/!*
```
Полный пример (IE8-, IE9 в режиме совместимости с IE8):
[codetabs src="leak-ie8-widget"]
### Утечка IE8 при обращении к коллекциям таблицы
Эта утечка происходит только в IE8 в стандартном режиме. В нём при обращении к табличным псевдо-массивам (напр. `rows`) создаются и не очищаются внутренние ссылки, что приводит к утечкам.
Также воспроизводится в новых IE в режиме совместимости с IE8.
Код:
```js
var elem = document.createElement('div'); // любой элемент
function leak() {
elem.innerHTML = '<table><tr><td>1</td></tr></table>';
*!*
elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке
// при том, что мы даже не сохраняем значение в переменную
*/!*
elem.removeChild(elem.firstChild); // удалить таблицу (*)
// alert(elem.childNodes.length) // выдал бы 0, elem очищен, всё честно
}
```
Полный пример (IE8):
[codetabs src="leak-ie8-table"]
Особенности:
<ul>
<li>Если убрать отмеченную строку, то утечки не будет.</li>
<li>Если заменить строку `(*)` на `elem.innerHTML = ''`, то память будет очищена, т.к. этот способ работает по-другому, нежели просто `removeChild` (см. главу [](/memory-management)).</li>
<li>Утечка произойдёт не только при доступе к `rows`, но и к другим свойствам, например `elem.firstChild.tBodies[0]`.</li>
</ul>
Эта утечка проявляется, в частности, при удалении детей элемента следующей функцией:
```js
function empty(elem) {
while (elem.firstChild) elem.removeChild(elem.firstChild);
}
```
Если идёт доступ к табличным коллекциям и регулярное обновление таблиц при помощи DOM-методов -- утечка в IE8 будет расти.
Более подробно вы можете почитать об этой утечке в статье [Утечки памяти в IE8, или страшная сказка со счастливым концом](http://habrahabr.ru/post/141451/).
### Утечка через XmlHttpRequest в IE8-
Следующий код вызывает утечки памяти в IE8-:
```js
function leak() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server.do', true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
// ...
}
}
xhr.send(null);
}
```
Как вы думаете, почему? Если вы внимательно читали то, что написано выше, то имеете информацию для ответа на этот вопрос..
Посмотрим, какая структура памяти создается при каждом запуске:
<img src="leak-xhr.svg">
Когда запускается асинхронный запрос `xhr`, браузер создаёт специальную внутреннюю ссылку (internal reference) на этот объект. находится в процессе коммуникации. Именно поэтому объект `xhr` будет жив после окончания работы функции.
Когда запрос завершен, браузер удаляет внутреннюю ссылку, `xhr` становится недостижимым и память очищается... Везде, кроме IE8-.
Полный пример (IE8):
[codetabs src="leak-ie8-xhr"]
Чтобы это исправить, нам нужно разорвать круговую ссылку `XMLHttpRequest ↔ JS`. Например, можно удалить `xhr` из замыкания:
```js
function leak() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'something.js?' + Math.random(), true);
xhr.onreadystatechange = function() {
if (xhr.readyState != 4) return;
if (xhr.status == 200) {
document.getElementById('test').innerHTML++;
}
*!*
xhr = null; // по завершении запроса удаляем ссылку из замыкания
*/!*
}
xhr.send(null);
}
```
<img src="xhr2.png">
Теперь циклической ссылки нет -- и не будет утечки.
## Объемы утечек памяти
Объем "утекающей" памяти может быть небольшим. Тогда это почти не ощущается. Но так как замыкания ведут к сохранению переменных внешних функций, то одна функция может тянуть за собой много чего ещё.
Представьте, вы создали функцию, и одна из ее переменных содержит очень большую по объему строку (например, получает с сервера).
```js
function f() {
var data = "Большой объем данных, например, переданных сервером"
/* делаем что-то хорошее (ну или плохое) с полученными данными */
function inner() {
// ...
}
return inner;
}
```
Пока функция `inner` остается в памяти, `LexicalEnvironment` с переменной большого объема внутри висит в памяти.
Висит до тех пор, пока функция `inner` жива.
Как правило, JavaScript не знает, какие из переменных функции `inner` будут использованы, поэтому оставляет их все. Исключение -- виртуальная машина V8 (Chrome, Opera, Node.JS), она часто (не всегда) видит, что переменная не используется во внутренних функциях, и очистит память.
В других же интерпретаторах, даже если код спроектирован так, что никакой утечки нет, по вполне разумной причине может создаваться множество функций, а память будет расти потому, что функция тянет за собой своё замыкание.
Сэкономить память здесь вполне можно. Мы же знаем, что переменная `data` не используется в `inner`. Поэтому просто обнулим её:
```js
function f() {
var data = "Большое количество данных, например, переданных сервером"
/* действия с data */
function inner() {
// ...
}
*!*
data = null; // когда data станет не нужна -
*/!*
return inner;
}
```
## Поиск и устранение утечек памяти
### Проверка на утечки
Существует множество шаблонов утечек и ошибок в браузерах, которые могут приводить к утечкам. Для их устранения сперва надо постараться изолировать и воспроизвести утечку.
<ul>
<li>**Необходимо помнить, что браузер может очистить память не сразу когда объект стал недостижим, а чуть позже.** Например, сборщик мусора может ждать, пока не будет достигнут определенный лимит использования памяти, или запускаться время от времени.</li>
Поэтому если вы думаете, что нашли проблему и тестовый код, запущенный в цикле, течёт -- подождите примерно минуту, добейтесь, чтобы памяти ело стабильно и много. Тогда будет понятно, что это не особенность сборщика мусора.</li><li>**Если речь об IE, то надо смотреть "Виртуальную память" в списке процессов, а не только обычную "Память".** Обычная может очищаться за счет того, что перемещается в виртуальную (на диск).</li>
<li>Для простоты отладки, если есть подозрение на утечку конкретных объектов, в них добавляют большие свойства-маркеры. Например, подойдет фрагмент текста: `new Array(999999).join('leak')`.</li>
</ul>
### Настройка браузера
Утечки могут возникать из-за расширений браузера, взимодействющих со страницей. Еще более важно, что **утечки могут быть следствием конфликта двух браузерных расширений** Например, было такое: память текла когда включены расширения Skype и плагин антивируса одновременно.
Чтобы понять, в расширениях дело или нет, нужно отключить их:
<ol>
<li>Отключить Flash.</li>
<li>Отключить анивирусную защиту, проверку ссылок и другие модули и дополнения.</li>
<li>Отключить плагины. Отключить ВСЕ плагины.
<ul>
<li>
Для IE есть параметр коммандной строки:
```
"C:\Program Files\Internet Explorer\iexplore.exe" -extoff
```
Кроме того необходимо отключить сторонние расширения в свойствах IE.
<img src="ie9_disable1.png">
<img src="ie9_disable2.png">
</li>
<li>Firefox необходимо запускать с чистым профилем. Используйте следующую команду для запуска менеджера профилей и создания чистого пустого профиля:
```
firefox --profilemanager
```
</li>
</ul>
</li>
</ol>
## Инструменты
Пожалуй, единственный браузер с поддержкой отладки памяти -- это Chrome. В инструментах разработчика вкладка Timeline -- Memory показывает график использования памяти.
<img src="chrome.png">
Можем посмотреть, сколько памяти и куда он использует.
Также в Profiles есть кнопка Take Heap Snapshot, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом, выяснять количество новых объектов. Можно смотреть, почему объект не очищен и кто на него ссылается.
Замечательная статья на эту тему есть в документации: [Chrome Developer Tools: Heap Profiling](http://code.google.com/chrome/devtools/docs/heap-profiling.html).
Утечки памяти штука довольно сложная. В борьбе с ними вам определенно понадобится одна вещь: *Удача!*
<img src="goodluck.png">

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=8">
</head>
<body>
<script>
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
function leak() {
// Создаём новый DIV, добавляем к BODY
var elem = document.createElement('div');
document.body.appendChild(elem);
elem.__expando = {
bigAss: new Array(1000000).join('lalala'),
method: function() {} // создаётся круговая ссылка через замыкание
};
// Удалить элемент из DOM. Браузер должен очистить память.
elem.parentElement.removeChild(elem);
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>

View file

@ -0,0 +1,34 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
var elem = document.createElement('div'); // любой элемент
// Течёт в настоящем IE8, Standards Mode
// Не течёт в других IE. Не течёт в IE9 в режиме совместимости с IE8
function leak() {
for (var i = 0; i < 2000; i++) {
elem.innerHTML = '<table><tr><td>1</td></tr></table>';
elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке
// при том, что мы даже без сохраненяем значение в переменную
elem.removeChild(elem.firstChild); // удалить таблицу
// elem.innerHTML = '' очистил бы память, он работает по-другому, см. главу "управление памятью"
}
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>

View file

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=8">
</head>
<body>
<script>
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
function leak() {
// Создаём новый DIV, добавляем к BODY
var elem = document.createElement('div');
document.body.appendChild(elem);
// Записываем в свойство ссылку на объект
var menu = new Menu(elem);
elem.menu = menu;
// Удалить элемент из DOM. Браузер должен очистить память.
elem.parentElement.removeChild(elem);
}
function Menu(elem) {
elem.onclick = function() {};
this.bigAss = new Array(1000000).join('lalala');
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<p>Страница создаёт объект <code>XMLHttpRequest</code> каждые 50мс.</p>
<p>Нажмите на кнопку и смотрите на память, она течёт в IE&lt;9.</p>
<button onclick="setInterval(leak, 50);">Запустить</button>
<script>
function leak() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '?' + Math.random(), true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
document.getElementById('test').innerHTML++;
}
}
xhr.send(null);
}
</script>
<div>Количество запросов: <span id="test">0</span></div>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=8">
</head>
<body>
<script>
// Утечка в IE8 standards mode, а также в IE9 в режиме IE8
// См. http://blog.j15r.com/2009/07/memory-leaks-in-ie8.html
function leak() {
// Создаём новый DIV, добавляем к BODY
var elem = document.createElement('div');
document.body.appendChild(elem);
// Записываем в свойство жирный объект
elem.__expando = {
bigAss: new Array(1000000).join('lalala')
};
// Создаём круговую ссылку. Без этой строки утечки не будет.
elem.__expando.__elem = elem;
// Удалить элемент из DOM. Браузер должен очистить память.
elem.parentElement.removeChild(elem);
}
</script>
<p>Нажимайте на кнопку и наблюдайте, как увеличивается количество занимаемой браузером памяти.</p>
<button onclick="leak()">Создать утечку!</button>
</body>
</html>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="488px" height="246px" viewBox="0 0 488 246" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: bin/sketchtool 1.3 (252) - http://www.bohemiancoding.com/sketch -->
<title>leak-xhr-2.svg</title>
<desc>Created with bin/sketchtool.</desc>
<defs></defs>
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="leak-xhr-2.svg" sketch:type="MSArtboardGroup">
<rect id="Rectangle-2" stroke="#CFCE95" stroke-width="2" fill="#F5F5D9" sketch:type="MSShapeGroup" x="50" y="39" width="117" height="38"></rect>
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="331" y="39" width="117" height="38"></rect>
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="50" y="167" width="398" height="38"></rect>
<text id="xhr" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#727155">
<tspan x="97" y="63">xhr</tspan>
</text>
<text id="function" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#727155">
<tspan x="359.210938" y="63">function</tspan>
</text>
<text id="LexicalEnvironment" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#924565">
<tspan x="180.224609" y="190">LexicalEnvironment</tspan>
</text>
<path d="M110.5,93.5 L110.5,144.5" id="Line" stroke="#B8BAC1" stroke-width="2" stroke-linecap="square" fill="#B8BAC1" sketch:type="MSShapeGroup" transform="translate(110.500000, 119.000000) scale(1, -1) translate(-110.500000, -119.000000) "></path>
<path id="Line-decoration-1" d="M110.5,144.5 C111.55,140.72 112.45,137.48 113.5,133.7 C111.4,133.7 109.6,133.7 107.5,133.7 C108.55,137.48 109.45,140.72 110.5,144.5 C110.5,144.5 110.5,144.5 110.5,144.5 Z" stroke="#B8BAC1" stroke-width="2" stroke-linecap="square" fill="#B8BAC1"></path>
<path d="M187,61 L311.169243,61" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
<path id="Line-decoration-1" d="M311,61 C307.22,59.95 303.98,59.05 300.2,58 C300.2,60.1 300.2,61.9 300.2,64 C303.98,62.95 307.22,62.05 311,61 C311,61 311,61 311,61 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
<path d="M381.5,113.5 L381.5,144.5" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
<path id="Line-decoration-1" d="M381.5,144.5 C382.55,140.72 383.45,137.48 384.5,133.7 C382.4,133.7 380.6,133.7 378.5,133.7 C379.55,137.48 380.45,140.72 381.5,144.5 C381.5,144.5 381.5,144.5 381.5,144.5 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
<text id="onreadystatechange" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
<tspan x="181.5" y="48">onreadystatechange</tspan>
</text>
<text id="[[Scope]]" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
<tspan x="346" y="102">[[Scope]]</tspan>
</text>
<path d="M99.5,110.5 L122.845236,133.845236" id="Line" stroke="#CB1E31" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
<path d="M122,110.5 L98.6547642,133.845236" id="Line-2" stroke="#CB1E31" stroke-linecap="square" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="488px" height="308px" viewBox="0 0 488 308" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: bin/sketchtool 1.3 (252) - http://www.bohemiancoding.com/sketch -->
<title>leak-xhr.svg</title>
<desc>Created with bin/sketchtool.</desc>
<defs></defs>
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="leak-xhr.svg" sketch:type="MSArtboardGroup">
<rect id="Rectangle-2" stroke="#CFCE95" stroke-width="2" fill="#F5F5D9" sketch:type="MSShapeGroup" x="50" y="101" width="117" height="38"></rect>
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="331" y="101" width="117" height="38"></rect>
<rect id="Rectangle-2" stroke="#C74A6C" stroke-width="2" fill="#FCDFE1" sketch:type="MSShapeGroup" x="50" y="229" width="398" height="38"></rect>
<text id="xhr" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#727155">
<tspan x="97" y="125">xhr</tspan>
</text>
<text id="function" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#727155">
<tspan x="359.210938" y="125">function</tspan>
</text>
<text id="LexicalEnvironment" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" sketch:alignment="middle" fill="#924565">
<tspan x="180.224609" y="252">LexicalEnvironment</tspan>
</text>
<path d="M110.5,154.5 L110.5,185.5" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup" transform="translate(110.500000, 170.000000) scale(1, -1) translate(-110.500000, -170.000000) "></path>
<path id="Line-decoration-1" d="M110.5,185.5 C111.55,181.72 112.45,178.48 113.5,174.7 C111.4,174.7 109.6,174.7 107.5,174.7 C108.55,178.48 109.45,181.72 110.5,185.5 C110.5,185.5 110.5,185.5 110.5,185.5 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
<path d="M187,123 L311.169243,123" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
<path id="Line-decoration-1" d="M311,123 C307.22,121.95 303.98,121.05 300.2,120 C300.2,122.1 300.2,123.9 300.2,126 C303.98,124.95 307.22,124.05 311,123 C311,123 311,123 311,123 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
<path d="M381.5,175.5 L381.5,206.5" id="Line" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47" sketch:type="MSShapeGroup"></path>
<path id="Line-decoration-1" d="M381.5,206.5 C382.55,202.72 383.45,199.48 384.5,195.7 C382.4,195.7 380.6,195.7 378.5,195.7 C379.55,199.48 380.45,202.72 381.5,206.5 C381.5,206.5 381.5,206.5 381.5,206.5 Z" stroke="#EE6B47" stroke-width="3" stroke-linecap="square" fill="#EE6B47"></path>
<path d="M112.5,46.5 L112.5,77.5" id="Line" stroke="#8A704D" stroke-width="2" stroke-linecap="square" fill="#8A704D" sketch:type="MSShapeGroup"></path>
<path id="Line-decoration-1" d="M112.5,77.5 C113.55,73.72 114.45,70.48 115.5,66.7 C113.4,66.7 111.6,66.7 109.5,66.7 C110.55,70.48 111.45,73.72 112.5,77.5 C112.5,77.5 112.5,77.5 112.5,77.5 Z" stroke="#8A704D" stroke-width="2" stroke-linecap="square" fill="#8A704D"></path>
<text id="property" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
<tspan x="78" y="208">property</tspan>
</text>
<text id="onreadystatechange" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
<tspan x="181.5" y="110">onreadystatechange</tspan>
</text>
<text id="внутренняя-ссылка" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
<tspan x="47" y="37">внутренняя ссылка</tspan>
</text>
<text id="[[Scope]]" sketch:type="MSTextLayer" font-family="Consolas" font-size="14" font-weight="normal" fill="#8A704D">
<tspan x="346" y="164">[[Scope]]</tspan>
</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,144 @@
# Утечки памяти при использовании jQuery
В jQuery для хранения обработчиков событий и других вспомогательных данных, связанных с DOM-элементами, используется внутренний объект, который в jQuery 1 доступен через <a href="http://api.jquery.com/jQuery.data/">$.data</a>.
В jQuery 2 доступ к нему закрыт через замыкание, он стал локальной переменной внутри jQuery с именем `data_priv`, но в остальном всё работает точно так, как описано, и с теми же последствиями.
## $.data
Встроенная функция `$.data` позволяет хранить привязывать произвольные значения к DOM-узлам.
Например:
```js
//+ no-beautify
// присвоить
$(document).data('prop', { anything: "любой объект" })
// прочитать
alert( $(document).data('prop').anything ) // любой объект
```
Реализована она хитрым образом. Данные не хранятся в самом элементе, а во внутреннем объекте jQuery.
jQuery-вызов `elem.data(prop, val)` делает следующее:
<ol>
<li>Элемент получает уникальный идентификатор, если у него такого еще нет:
```js
elem[jQuery.expando] = id = ++jQuery.uuid; // средствами jQuery
```
`jQuery.expando` -- это случайная строка, сгенерированная jQuery один раз при входе на страницу. Уникальное свойство, чтобы ничего важного не перезаписать.</li>
<li>...А сами данные сохраняются в специальном объекте `jQuery.cache`:
```js
//+ no-beautify
jQuery.cache[id]['prop'] = { anything: "любой объект" };
```
</li>
</ol>
Когда данные считываются из элемента:
<ol>
<li>Уникальный идентификатор элемента извлекается из `id = elem[ jQuery.expando]`.
<li>Данные считываются из `jQuery.cache[id]`.</li>
</ol>
Смысл этого API в том, что DOM-элемент никогда не ссылается на JavaScript объект напрямую. Задействуется идентификатор, а сами данные хранятся в `jQuery.cache`. Утечек в IE не будет.
К тому же все данные известны библиотеке, так что можно клонировать с ними и т.п.
Как побочный эффект -- возникает утечка памяти, если элемент удален из DOM без дополнительной очистки.
## Примеры утечек в jQuery
Следующая функция `leak` создает jQuery-утечку во всех браузерах:
```html
<!--+ run -->
<script src="http://code.jquery.com/jquery.min.js"></script>
<div id="data"></div>
<script>
function leak() {
*!*
$('<div/>')
.html(new Array(1000).join('text'))
.click(function() {})
.appendTo('#data');
document.getElementById('data').innerHTML = '';
*/!*
}
var interval = setInterval(leak, 10)
</script>
Утечка идёт...
<input type="button" onclick="clearInterval(interval)" value="stop" />
```
Утечка происходит потому, что обработчик события в jQuery хранится в данных элемента. В строке `(*)` элемент удален очисткой родительского `innerHTML`, но в `jQuery.cache` данные остались.
Более того, система обработки событий в jQuery устроена так, что вместе с обработчиком в данных хранится и ссылка на элемент, так что в итоге оба -- и обработчик и элемент -- остаются в памяти вместе со всем замыканием!
Ещё более простой пример утечки:
Этот код также создает утечку:
```js
function leak() {
$('<div/>')
.click(function() {})
}
```
...То есть, мы создаём элемент, вешаем на него обработчик... И всё.
Такой код ведёт к утечке памяти как раз потому, что элемент `<div>` создан, но нигде не размещен :). После выполнения функции ссылка на него теряется. Но обработчик события `click` уже сохранил данные в `jQuery.cache`, которые застревают там навсегда.
## Используем jQuery без утечек
Чтобы избежать утечек, описанных выше, для удаления элементов используйте функции jQuery API, а не чистый JavaScript.
Методы <a href="http://api.jquery.com/remove/">remove()</a>, <a href="http://api.jquery.com/empty">empty()</a> и <a href="http://api.jquery.com/html">html()</a> проверяют дочерние элементы на наличие данных и очищают их. Это несколько замедляет процедуру удаления, но зато освобождается память.
К счастью обнаружить такие утечки легко. Проверьте размер `$.cache`. Если он большой и растет, то изучите кэш, посмотрите, какие записи остаются и почему.
## Улучшение производительности jQuery
У способа организации внутренних данных, применённого в jQuery, есть важный побочный эффект.
Функции, удаляющие элементы, также должны удалить и связанные с ними внутренние данные. Для этого нужно для каждого удаляемого элемента проверить -- а нет ли чего во внутреннем хранилище? И, если есть -- удалить.
Представим, что у нас есть большая таблица `<table>`, и мы хотим обновить её содержимое на новое. Вызов `$('table').html(новые данные)` перед вставкой новых данных аккуратно удалит старые: пробежит по всем ячейкам и проверит внутреннее хранилище.
Если это большая таблица, то обработчики, скорее всего, стоят не на ячейках, а на самом элементе `<table>`, то есть используется делегирование. А, значит, тратить время на проверку всех подэлементов ни к чему.
Но jQuery-то об этом не знает!
Чтобы "грязно" удалить элемент, без чистки, мы можем сделать это через "обычные" DOM-вызовы или воспользоваться методом <a href="http://api.jquery.com/detach">detach()</a>. Его официальное назначение -- в том, чтобы убрать элемент из DOM, но сохранить возможность для вставки (и, соответственно, оставить на нём все данные). А неофициальное -- быстро убрать элемент из DOM, без чистки.
Возможен и промежуточный вариант: никто не мешает сделать `elem.detach()` и поместить вызов `elem.remove()` в `setTimeout`. В результате очистка будет происходить асинхронно и незаметно.
## Итого
<ul>
<li>Утечки памяти при использовании jQuery возможны, если через DOM-методы удалять элементы, к которым привязаны данные или обработчики.</li>
<li>Чтобы утечки не было, достаточно убедиться, что элемент удаляется при помощи методов jQuery.</li>
<li>Побочный эффект -- при удалении элементов jQuery должна проверить наличие данных для них. Это сильно замедляет процесс удаления большого поддерева DOM.</li>
<li>Если мы значем, что обработчиков и данных нет -- гораздо быстрее удалять элементы при помощи вызова `detach` или обычного DOM.</li>
</ul>

View file

@ -0,0 +1,147 @@
# Очистка памяти при removeChild/innerHTML
Управление памятью в случае с DOM работает по сути так же, как и с обычными JavaScript-объектами. Пока объект достижим -- он остаётся в памяти.
Но есть и особенности, поскольку DOM весь переплетён ссылками.
[cut]
## Пример
Для примера рассмотрим следующий HTML:
```html
<html>
<body>
<div>
<ul>
<li>Список</li>
</ul>
Сосед
</div>
</body>
</html>
```
Его DOM (показаны только основные ссылки):
<img src="html.png">
## Удаление removeChild
Операция `removeChild` разрывает все связи удаляемым узлом и его родителем.
Поэтому, если удалить `DIV` из `BODY`, то всё поддерево под `DIV` станет недостижимым и будет удалено.
А что происходит, если на какой-то элемент внутри удаляемого поддерева есть ссылка?
Например, `UL` сохранён в переменную `list`:
```js
var list = document.getElementsByTagName('UL')[0];
document.body.removeChild(document.body.children[0]);
```
В этом случае, так как из этого `UL` можно по ссылкам добраться до любого другого места DOM, то получается, что все объекты по-прежнему достижимы и должны остаться в памяти:
<img src="html-list.png">
То есть, DOM-объекты при использовании `removeChild` работают по той же логике, что и обычные объекты.
## Удаление через innerHTML
А вот удаление через очистку `elem.innerHTML="..."` браузеры интерпретируют по-разному.
По идее, при присвоении `elem.innerHTML=html` из DOM должны удаляться предыдущие узлы и добавляться новые, из указанного `html`. Но стандарт ничего не говорит о том, что делать с узлами после удаления. И тут разные браузеры имеют разное мнение.
Посмотрим, что произойдёт с DOM-структурой при очистке `BODY`, если на какой-либо элемент есть ссылка.
```js
var list = document.getElementsByTagName('UL')[0];
document.body.innerHTML = "";
```
Обращаю внимание -- связь разрывается только между `DIV` и `BODY`, т.е. на верхнем уровне, а `list` -- это произвольный элемент.
Чтобы увидеть, что останется в памяти, а что нет -- запустим код:
```html
<!--+ run -->
<div>
<ul>
<li>Список</li>
</ul>
Сосед
</div>
<script>
var list = document.getElementsByTagName('ul')[0];
document.body.innerHTML = ''; // удалили DIV
alert( list.parentNode ); // цела ли ссылка UL -> DIV ?
alert( list.nextSibling ); // живы ли соседи UL ?
alert( list.children.length ); // живы ли потомки UL ?
</script>
```
Как ни странно, браузеры ведут себя по-разному:
<table>
<thead>
<tr><th></th>
<th>`parentNode`</th>
<th>`nextSibling`</th>
<th>`children.length`</th>
</tr>
</thead>
<tbody>
<tr>
<td>Chrome/Safari/Opera</td>
<td>`null`</td>
<td>`null`</td>
<td>`1`</td>
</tr>
<tr>
<td>Firefox</td>
<td>узел DOM</td>
<td>узел DOM</td>
<td>`1`</td>
</tr>
<tr>
<td>IE 11-</td>
<td>`null`</td>
<td>`null`</td>
<td>`0`</td>
</tr>
</tbody>
</table>
Иными словами, браузеры ведут себя с различной степенью агрессивности по отношению к элементам.
<dl>
<dt>Firefox</dt>
<dd>Главный пацифист. Оставляет всё, на что есть ссылки, т.е. элемент, его родителя, соседей и детей, в точности как при `removeChild`.</dd>
<dt>Chrome/Safari/Opera</dt>
<dd>Считают, что раз мы задали ссылку на `UL`, то нам нужно только это поддерево, а остальные узлы (соседей, родителей) можно удалить.</dd>
<dt>Internet Explorer</dt>
<dd>Как ни странно, самый агрессивный. Удаляет вообще всё, кроме узла, на который есть ссылка. Это поведение одинаково для всех версий IE.</dd>
</dl>
На иллюстрации ниже показано, какую часть DOM оставит каждый из браузеров:
<img src="html-innerhtml.png">
## Итого
Если на какой-то DOM-узел есть ссылка, то:
<ul>
<li>При использовании `removeChild` на родителе (или на этом узле, не важно) все узлы, достижимые из данного, остаются в памяти.
То есть, фактически, в памяти может остаться большая часть дерева DOM. Это даёт наибольшую свободу в коде, но может привести к большим "утечкам памяти" из-за сохранения данных, которые реально не нужны.</li>
<li>При удалении через `innerHTML` браузеры ведут себя с различной степенью агрессивности. Кросс-браузерно гарантировано одно: сам узел, на который есть ссылка, останется в памяти.
Поэтому обращаться к соседям и детям узла, предок которого удалён через присвоение `innerHTML`, нельзя.</li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

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>

View file

@ -0,0 +1,177 @@
# GCC: статическая проверка типов
Google Closure Compiler, как и любой кошерный компилятор, старается проверить правильность кода и предупредить о возможных ошибках.
Первым делом он, разумеется, проверяет структуру кода и сразу же выдает такие ошибки как пропущенная скобка или лишняя запятая.
Но, кроме этого, он умеет проверять типы переменных, используя как свои собственные знания о встроенных javascript-функциях и преобразованиях типов,
так и информацию о типах из JSDoc, указываемую javascript-разработчиком.
Это обеспечивает то, чем так гордятся компилируемые языки -- статическую проверку типов, что позволяет избежать лишних ошибок во время выполнения.
[cut]
Для вывода предупреждений при проверки типов используется флаг `--jscomp_warning checkTypes`.
## Задание типа при помощи аннотации
Самый очевидный способ задать тип -- это использовать аннотацию. Полный список аннотаций вы найдете в <a href="http://code.google.com/intl/ru/closure/compiler/docs/js-for-compiler.html">документации</a>.
В следующем примере параметр <code>id</code> функции <code>f1</code> присваивается переменной <code>boolVar</code> другого типа:
```js
/** @param {number} id */
function f(id) {
/** @type {boolean} */
var boolVar;
boolVar = id; // (!)
}
```
Компиляция с флагом `--jscomp_warning checkTypes` выдаст предупреждение:
```
f.js:6: WARNING - assignment
found : number
required: boolean
boolVar = id; // (!)
^
```
Действительно: произошло присвоение значения типа <code>number</code> переменной типа <code>boolean</code>.
Типы отслеживаются по цепочке вызовов.
Еще пример, на этот раз вызов функции с некорректным параметром:
```js
/** @param {number} id */
function f1(id) {
f2(id); // (!)
}
/** @param {string} id */
function f2(id) {}
```
Такой вызов приведёт к предупреждению со стороны минификатора:
```
f2.js:3: WARNING - actual parameter 1 of f2 does not match formal parameter
found : number
required: string
f2(id); // (!)
^
```
Действительно, вызов функции <code>f2</code> произошел с числовым типом вместо строки.
**Отслеживание приведений и типов идёт при помощи графа взаимодействий и выведению (infer) типов, который строит GCC по коду.**
## Знания о преобразовании типов
Google Closure Compiler знает, как операторы javascript преобразуют типы. Такой код уже не выдаст ошибку:
```js
/** @param {number} id */
function f1(id) {
/** @type {boolean} */
var boolVar;
boolVar = !!id
}
```
Действительно - переменная преобразована к типу boolean двойным оператором НЕ.
А код <code>boolVar = 'test-'+id</code> выдаст ошибку, т.к. конкатенация со строкой дает тип <code>string</code>.
## Знание о типах встроенных функций, объектные типы
Google Closure Compiler содержит описания большинства встроенных объектов и функций javascript вместе с типами параметров и результатов.
Например, объектный тип <code>Node</code> соответствует узлу DOM.
Пример некорректного кода:
```js
/** @param {Node} node */
function removeNode(node) {
node.parentNode.removeChild(node)
}
document.onclick = function() {
removeNode("123")
}
```
Выдаст предупреждение
```
f3.js:7: WARNING - actual parameter 1 of removeNode does not match formal parameter
found : string
required: (Node|null)
removeNode("123")
^
```
Обратите внимание - в этом примере компилятор выдает <code>required: Node|null</code>. Это потому, что указание объектного типа (не элементарного) подразумевает, что в функцию может быть передан <code>null</code>.
В следующем примере тип указан жестко, без возможности обнуления:
```js
*!*
/** @param {!Node} node */
*/!*
function removeNode(node) {
node.parentNode.removeChild(node)
}
```
Восклицательный знак означает, что параметр обязатален.
Найти описания встроенных типов и объектов javascript вы можете в файле экстернов: <code>externs.zip</code> находится в корне архива <code>compiler.jar</code>.
## Интеграция с проверками типов из Google Closure Library
В Google Closure Library есть функции проверки типов: <code>goog.isArray</code>, <code>goog.isDef</code>, <code>goog.isNumber</code> и т.п.
Google Closure Compiler знает о них и понимает, что внутри следующего <code>if</code> переменная может быть только функцией:
```js
var goog = {
isFunction: function(f) {
return typeof f == 'function'
}
}
if (goog.isFunction(func)) {
func.apply(1, 2)
}
```
Сжатие с проверкой выдаст предупреждение:
```
f.js:6: WARNING - actual parameter 2 of Function.apply does not match formal parameter
found : number
required: (Object|null|undefined)
func.apply(1, 2)
^ ^
```
То есть, компилятор увидел, что код, использующий <code>func</code> находится в `if (goog.isFunction(func))` и сделал соответствующий вывод, что это в этой ветке `func` является функцией, а значит вызов `func.apply(1,2)` ошибочен (второй аргумент не может быть числом).
Дело тут именно в интеграции с Google Closure Library. Если поменять `goog` на `g` -- предупреждения не будет.
## Резюме
Из нескольких примеров, которые мы рассмотрели, должна быть понятна общая логика проверки типов.
Соответствующие различным типам и ограничениям на типы аннотации вы можете найти в <a href="http://code.google.com/intl/ru/closure/compiler/docs/js-for-compiler.html">Документации Google</a>. В частности, возможно указание нескольких возможных типов, типа <code>undefined</code> и т.п.
Также можно указывать количество и тип параметров функции, ключевого слова <code>this</code>, объявлять классы, приватные методы и интерфейсы.
Проверка типов javascript, предоставляемая Google Closure Compiler -- пожалуй, самая продвинутая из существующих на сегодняшний день.
C ней аннотации, документирующие типы и параметры, становятся не просто украшением, а реальным средством проверки, уменьшающим количество ошибок на production.
Очень подробно проверка типов описана в книге [Closure: The Definitive Guide](http://www.ozon.ru/context/detail/id/6089988/), автора Michael Bolin.

View file

@ -0,0 +1,180 @@
# GCC: интеграция с Google Closure Library
Google Closure Compiler содержит ряд специальных возможностей для интеграции с Google Closure Library.
Здесь важны две вещи.
<ol>
<li>Для их использования возможно использовать минимум от Google Closure Library. Например, взять одну или несколько функций из библиотеки.</li>
<li>GCC -- расширяемый компилятор, можно добавить к нему свои "фазы оптимизации" для интеграции с другими инструментами и фреймворками.</li>
</ol>
[cut]
Интеграция с Google Closure Library подключается флагом <code>--process_closure_primitives</code>, который по умолчанию установлен в <code>true</code>. То есть, она включена по умолчанию.
Этот флаг запускает специальный проход компилятора, описанный классом <code>ProcessClosurePrimitives</code> и подключает дополнительную проверку типов <code>ClosureReverseAbstractInterpreter</code>.
Мы рассмотрим все действия, которые при этом происходят, а также некоторые опции, которые безопасным образом используют символы Google Closure Library без объявления флага.
## Преобразование основных символов
Следующие действия описаны в классе <code>ProcessClosurePrimitives</code>.
### Замена константы <code>COMPILED</code>
В Google Closure Library есть переменная:
```js
/**
* @define {boolean} ...
*/
var COMPILED = false;
```
Проход <code>ProcessClosurePrimitives</code> переопределяет ее в <code>true</code> и использует это при оптимизациях, удаляя ветки кода, не предназначены для запуска на production.
Такие функции существуют, например, в ядре Google Closure Library. К ним в первую очередь относятся вызовы, предназначенные для сборки и проверки зависимостей. Они содержат код, обрамленный проверкой <code>COMPILED</code>, например:
```js
goog.require = function(rule) {
// ...
if (!COMPILED) {
// основное тело функции
}
}
```
Аналогично может поступить и любой скрипт, даже без использования Google Closure Library:
```js
/** @define {boolean} */
var COMPILED = false
Framework = {}
Framework.sayCompiled = function() {
if (!COMPILED) {
alert("Not compressed")
} else {
alert("Compressed")
}
}
```
Для того, чтобы сработало, нужно сжать в продвинутом режиме:
```js
Framework = {};
Framework.sayCompiled = Framework.a = function() {
alert( "Compressed" );
};
```
Компилятор переопределил <code>COMPILED</code> в <code>true</code> и произвел соответствующие оптимизации.
### Автоподстановка локали
В Google Closure Compiler есть внутренняя опция <code>locale</code>
Эта опция переопределяет переменную <code>goog.LOCALE</code> на установленную при компиляции.
Для использования опции <code>locale</code>, на момент написания статьи, ее нужно задать в Java коде компилятора, т.к. соответствующего флага нет.
Как и <code>COMPILED</code>, константу <code>goog.LOCALE</code> можно и использовать в своем коде без библиотеки Google Closure Library.
### Проверка зависимостей
Директивы <code>goog.provide</code>, <code>goog.require</code>, <code>goog.addDependency</code> обрабатываются особым образом.
Все зависимости проверяются, а сами директивы проверки -- удаляются из сжатого файла.
### Экспорт символов
Вызов <code>goog.exportSymbol</code> задаёт экспорт символа.
Если подробнее, то код <code>goog.exportSymbol('a',myVar)</code> эквивалентен
`window['a'] = myVar`.
### Автозамена классов CSS
Google Closure Library умеет преобразовывать классы CSS на более короткие по списку, который задаётся при помощи `goog.setCssNameMapping`.
Например, следующая функция задает такой список.
```js
goog.setCssNameMapping({
"goog-menu": "a",
"goog-menu-disabled": "a-b",
"CSS_LOGO": "b",
"hidden": "c"
});
```
Тогда следующий вызов преобразуется в "a a-b":
```js
goog.getCssName('goog-menu') + ' ' + goog.getCssName('goog-menu', 'disabled')
```
Google Closure Compiler производит соответствующие преобразования в сжатом файле и удаляет вызов <code>setCssNameMapping</code> из кода.
Чтобы это сжатие работало, в HTML/CSS классы тоже должны сжиматься. По всей видимости, в приложениях Google это и происходит, но соответствующие инструменты закрыты от публики.
### Генерация списка экстернов
При объявлении внутренней опции <code>externExportsPath</code>, содержащей путь к файлу, в этот файл будут записаны все экспорты, описанные через <code>goog.exportSymbol</code>/<code>goog.exportProperty</code>.
В дальнейшем этот файл может быть использован как список экстернов для компиляции.
Эта опция может быть полезна для создания внешних библиотек, распространяемых со списком экстернов.
Для её использования нужна своя обёртка вокруг компилятора на Java. Соответствующий проход компилятора описан в классе <code>ExternExportsPass</code>.
### Проверка типов
В Google Closure Library есть ряд функций для проверки типов. Например: <code>goog.isArray</code>, <code>goog.isString</code>, <code>goog.isNumber</code>, <code>goog.isDef</code> и т.п.
Компилятор использует их для проверки типов, более подробно см. [](/gcc-check-types)
Эта логика описана в классе <code>ClosureReverseAbstractInterpreter</code>. Названия функций, определяющих типы, жестко прописаны в Java-коде, поменять их на свои без модификации исходников нельзя.
### Автогенерация экспортов из аннотаций
Для этого в Google Closure Compiler есть внутренняя опция <code>generateExports</code>.
Эта недокументированная опция добавляет проход компилятора, описанный классом <code>GenerateExports</code>.
Он читает аннотации <code>@export</code> и создает из них экспортирующие вызовы <code>goog.exportSymbol/exportProperty</code>. Название экспортирующих функций находится в классе соглашений кодирования, каким по умолчанию является <code>GoogleCodingConvention</code>.
Например:
```js
/** @export */
function Widget() {}
/** @export */
Widget.prototype.hide = function() {
this.elem.style.display = 'none'
}
```
После компиляции в продвинутом режиме:
```js
function a() {}
goog.d("Widget", a);
a.prototype.a = function() {
this.b.style.display = "none"
};
goog.c(a.prototype, "hide", a.prototype.a);
```
Свойства благополучно экспортированы. Удобно.
### Резюме
Google Closure Compiler содержит дополнительные фичи, облегчающие интеграцию с Google Closure Library. Некоторые из них весьма полезны, но требуют создания своего Java-файла, который ставит внутренние опции.
При обработке символов компилятор не смотрит, подключена ли библиотека, он находит обрабатывает их просто по именам. Поэтому вы можете использовать свою реализацию соответствующих функций.
Google Closure Compiler можно легко расширить, добавив свои опции и проходы оптимизатора, для интеграции с вашими инструментами.