Javascript.RU

Продвинутые оптимизации

Update: Более новый материал по этой теме находится по адресу https://learn.javascript.ru/multi-insert.

Продвинутые оптимизации google closure compiler включаются опцией --compilation_level ADVANCED_OPTIMIZATIONS.

Слово "продвинутые" (advanced) здесь, пожалуй, не совсем подходит. Кардинальное отличие этих оптимизаций от обычных (simple) - в том, что они небезопасны.

Чтобы ими пользоваться - надо уметь это делать.

Если в безопасном режиме переименовываются только переменные внутри функций, то в "продвинутом" - на более короткие имена заменяется все.

Если в безопасном режиме удаляется недостижимый код после return, то в продвинутом - вообще весь код, который не вызывается в явном виде.

Например, если запустить продвинутую оптимизацию на таком коде:

// my.js
function test(node) {
    node.innerHTML = "newValue"
}

Строка запуска компилятора:

java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js my.js

...То результат будет - пустой файл. Google Closure Compiler увидит, что функция test не используется и с чистой совестью вырежет ее.

А в следующем скрипте функция сохранится:

function test(n) {
    alert("this is my test number "+n)
}
test(1)
test(2)

После сжатия:

function a(b) {
  alert("this is my test number " + b)
}
a(1);
a(2);

Здесь в скрипте присутствует явный вызов функции, поэтому она сохранилась.

Конечно, есть способы, чтобы сохранить функции, вызов которых происходит вне скрипта, и мы их обязательно рассмотрим.

Режим "Advanced" не предусматривает сохранения глобальных переменных, наоборот - он переименовывает (и удаляет) все символы, кроме некоторых зарезервированных.

Иначе говоря, продвинутый режим (ADVANCED_OPTIMIZATIONS), в отличие от простого (SIMPLE_OPTIMIZATIONS - по умолчанию), вообще не заботится о доступности кода извне и сохранении ссылочной целостности относительно внешних скриптов.

Единственное, что он гарантирует - это внутреннюю ссылочную целостность, и то - при соблюдении ряда условий и практик программирования.

Собственно, за счет такого агрессивного подхода и достигается дополнительный эффект оптимизации и сжатия скриптов.

То есть, продвинутый режим - это не просто "улучшенный обычный", а принципиально другой, небезопасный и обфусцирующий подход к сжатию.

В этом коренное отличие Google Closure Compiler от компиляторов типа YUI Compressor и ShrinkSafe.

Для того, чтобы эффективно сжимать Google Closure Compiler в продвинутом режиме, нужно понимать, что и как он делает. Это мы сейчас обсудим.

Чтобы использовать сжатый скрипт, мы должны иметь возможность вызывать функции под теми именами, которые им дали.

То есть, перед нами стоит задача сохранения ссылочной целостности, которая заключается в том, чтобы обеспечить доступность нужных функций для обращений по исходному имени извне скрипта.

Существует два способа сохранения внешней ссылочной целостности для выборанных символов: экстерны и экспорты. Мы в подробностях рассмотрим оба, начнем с экстернов, т.к. они проще.

Экстерн - символ, который числится в специальном списке компилятора. Он должен быть определен вне скрипта, в файле экстернов.

Компилятор никогда не переименовывает экстерны. Например:

document.onkeyup = function(event) { alert(event.type) }

После продвинутого сжатия:

document.onkeyup = function(a) { alert(a.type) }

Как видите, переименованной оказалась только переменная event. Такое переименование заведомо безопасно, т.к. event - локальная переменная.

Почему компилятор не тронул остального? Попробуем другой вариант:

document.blabla = function(event) { alert(event.megaProperty) }

После компиляции:

document.a = function(a) { alert(a.b) }

Теперь компилятор переименовал и blabla и megaProperty.

Дело в том, что названия, использованные до этого, были во внутреннем списке экстернов компилятора. Этот список охватывает основные объекты браузеров и находится (под именем externs.zip) в корне архива compiler.jar или в директории externs в svn.

Компилятор никогда не переименовывает символы из списка экстернов, кроме случая, когда так названа локальная переменная.

Еще один пример:

window.resetNode = function(node) {
	var innerHTML = "test"
	node.innerHTML = innerHTML
}

На выходе:

window.a = function(a) {
  a.innerHTML = "test"
};

Как видите, внутренняя переменная innerHTML не просто переименована - она заинлайнена (заменена на значение). Так как эта переменная локальна, то любые действия внутри функции с ней безопасны.

А свойство innerHTML не тронуто, как и объект window - так как они в списке экстернов и не являются локальными переменными.

Это приводит к следующему побочному эффекту. Иногда свойства, которые следовало бы сжать, не сжимаются. Например:

window['User'] = function(name, type, age) {
	this.name = name
	this.type = type
	this.age = age
}

После сжатия:

window.User = function(a, b, c) {
  this.name = a;
  this.type = b;
  this.a = c
};

Как видно, свойство age сжалось, а name и type - нет. Это побочный эффект экстернов: name и type - в списке объектов браузера, и компилятор просто старается не наломать дров.

Поэтому отметим еще одно полезное правило оптимизации:

Названия своих свойств не должны совпадать с зарезервированными словами (экстернами). Тогда они будут хорошо сжиматься.

Для задания списка экстернов их достаточно перечислить в файле и указать этот файл флагом --externs <файл экстернов.js>.

При перечислении объектов в файле экстернов - объявляйте их и перечисляйте свойства. Все эти объявления никуда не идут, они используются только для создания списка, который обрабатывается компилятором.

Пример: myexterns.js
var dojo = {}
dojo._scopeMap;

Использование такого файла при сжатии (опция --externs myexterns.js) приведет к тому, что все обращения к символам dojo и к dojo._scopeMap будут не сжаты, а оставлены "как есть".

Кстати, бывает удобно разрешить переименование для некоторых экстернов. Это делается через присваивание их промежуточной переменной-неэкстерну.

В основном, это работает с глобальными переменными и статическими методами.

Например, мы хотим сжать document в следующем фрагменте:

function doSomething() {
  var elem = document.createElement("div")
  document.appendChild(elem)
}

Для этого присвоим document промежуточной переменной doc:

var doc = document
function doSomething() {
  var elem = doc.createElement("div")
  doc.appendChild(elem)
}

Компилятор не имеет права сжимать document, а вот все обращения к doc будут сжаты, т.к имя переменной doc будет заменено на более короткое. В результате, в сжатом коде document появится лишь один раз, а все остальные обращения (через doc) будут сжаты.

Экспорт - программный ход, основанный на следующем правиле поведения компилятора.

Компилятор заменяет обращения к свойствам через кавычки на точку, и при этом не трогает название свойства.

Например, window['User'] превратится в window.User, но не дальше.

Таким образом можно экспортировать нужные функции и объекты:

function SayWidget(elem) {
	this.elem = elem

	this.init()
}
window['SayWidget'] = SayWidget

На выходе:

function a(b) {
  this.a = b;
  this.b()
}
window.SayWidget = a;

Обратим внимание - сама функция SayWidget была переименована в a. Но затем - экспортирована как window.SayWidget, и таким образом доступна внешним скриптам.

Добавим пару методов в прототип:

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

После сжатия:

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;

Благодаря строке

SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler

метод setSayHandler экспортирован и доступен для внешнего вызова.

Сама строка экспорта выглядит довольно глупо. По виду - присваиваем свойство самому себе.
Но логика сжатия google closure compiler работает так, что такая конструкция является экспортом. Справа переименование свойства setSayHandler происходит, а слева - нет.

Кстати, google closure compiler больше любит другую организацию свойств объекта при ООП, но об этом - чуть позже.

Планируйте жизнь после сжатия.

Рассмотрим следующий код:

window['Animal'] = function() {
	this.blabla = 1
	this['blabla'] = 2
}

После сжатия:

window.Animal = function() {
  this.a = 1;
  this.blabla = 2
};

Как видите, первое обращение к свойству blabla сжалось, а второе (как и все аналогичные) - преобразовалось в синтаксис через точку.
В результате получили некорректное поведение кода.

Так что, используя продвинутый режим оптимизации, планируйте поведение кода после сжатия, особенно при обращении к свойствам через квадратные скобки: object[methodName].

В библиотеке Google Closure Library для экспорта есть специальная функция goog.exportSymbol. Вызывается так:

goog.exportSymbol('my.SayWidget', SayWidget)

Эта функция по сути работает также, как и рассмотренная выше строка с присвоением свойства, но при необходимости создает нужные объекты.

Она аналогична коду:

window['my'] = window['my'] || {}
window['my']['SayWidget'] = SayWidget

То есть, если путь к объекту не существует - exportSymbol создаст нужные пустые объекты.

Функция goog.exportProperty экспортирует свойство объекта:

goog.exportProperty(SayWidget.prototype, 'setSayHandler', SayWidget.prototype.setSayHandler)

Строка выше - то же самое, что и:

SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
Зачем нужны exportSymbol и exportProperty?

Действительно, зачем, если все можно сделать простым присваиванием?

Основная цель этих функций - во взаимодействии с Google Closure Compiler. Они дают информацию компилятору об экспортах, которую он может использовать.

Например, есть недокументированная внутренняя опция externExportsPath, которая генерирует из всех экспортов файл экстернов. Таким образом можно распространять откомпилированный javascript-файл как внешнюю библиотеку, с файлом экстернов для удобного внешнего связывания.

Кроме того, экспорт через эти функциями удобен и нагляден.

Если вы используете продвинутый режим оптимизации, то можно подключить файл base.js из Google Closure Library просто чтобы работали эти функции.
Оптимизатор при продвинутом сжатии вырежет из base.js (почти) все лишнее, кроме функций экспорта, так что overhead будет минимальным.

Между экспортом и экстерном есть кое-что общее. И то и другое дает возможность доступа к объектам под исходным именем, до переименования.

Но, в остальном, это совершенно разные вещи.

Экстерн Экспорт
Служит для тотального запрета на переименование всех обращений к свойству.
Задумано для сохранения обращений к стандартным объектам браузера, внешним библиотекам.
Служит для открытия доступа к свойству извне под указанным именем.
Задумано для открытия внешнего интерфейса к сжатому скрипту.
Работает со свойством, объявленным вне скрипта.
Вы не можете объявить новое свойство в скрипте и сделать его экстерном.
Создает ссылку на свойство, объявленное в скрипте.
Если window - экстерн, то все обращения к window в скрипте останутся как есть. Если user экспортируется, то создается только одна ссылка под полным именем, а все остальные обращения будут сокращены.

Еще один важный принцип работы продвинутого режима заключается в инлайнинге констант и функций.

Инлайнинг - это замена всех вызовов функции на код этой функции и, аналогично, замена всех обращений к константе на ее значение.

Например:

function test(n) {
    alert("this is my test number "+n)
}
test(1)

После сжатия в продвинутом режиме:

alert("this is my test number 1");

Функция test не является экстерном и не экспортируема, поэтому нет никаких требований к тому, чтобы ее сохранять.

Поэтому компилятор заменил вызов test(1) на тело функции. Кроме того, конкретно в этом примере, произошло объединение констант:"this is my test number"+1.

Константы инлайнятся аналогично функциям.

Например:

var MULTIPLER = 10
function test(n) {
    alert("this is my test number: "+n*MULTIPLER)
}
test(1)

После сжатия:

alert("this is my test number: 10");

Важно понимать, что инлайнинг происходит только тогда, когда он приводит к уменьшению размера кода.

Например:

function test(n) {
    alert("this is my test number "+n)
}
test(1)

После сжатия:

function a(b) {
  alert("this is my test number " + b)
}
a(1);
a(2);

Здесь компилятор посчитал потенциальную выгоду от замены функции на ее содержание и увидел, что инлайнинг увеличит размер кода. Поэтому функция просто переименована.

Если в обычном режиме сжатия стиль программирования - "как можно больше локальных переменных, как можно больше внутренних функций", то при использовании продвинутого режима все с точностью наоборот.

Функции надо как можно лучше отделять друг от друга. Поток выполнения должен быть как можно более понятный компилятору.

Например, для обычного режима сжатия типично такое объявление библиотеки:

(function(){

// локальная переменная для переименования компрессором
var document = window.document

// пространство имен и локальная перменная для него
var MyFramework = window.MyFramework = { }

// нужная функция
MyFramework.used = function() { 
	document.createElement('div')
}

// ненужная функция
MyFramework.unused = function() {
	alert("unused")
}

})();

// использование
MyFramework.used()

Оно удобно, т.к. позволяет внутри внешней функции объявлять любые методы, временные переменные, делать загрузочные действия и т.п.

Такой стиль используется в большинстве javascript-библиотек на момент написания этой статьи.

Но теперь посмотрим на результат компиляции. В обычном режиме:

(function() {
  var b = window.document, a = window.MyFramework = {};
  a.used = function() {
    b.createElement("div")
  };
  a.unused = function() {
    alert("unused")
  }
})();
MyFramework.used();

Это - примерно то, что мы ожидали от безопасного режима. Ненужный метод остался, локальные свойства переименованы.

А теперь продвинутый режим:

(function() {
  var b = window.document, a = window.b = {};
  a.a = function() {
    b.createElement("div")
  };
  a.c = function() {
    alert("unused")
  }
})();
MyFramework.a();

Полученный код не то, что недосжат: оставлен заведомо ненужный метод, он просто-напросто перестал работать после сжатия. Google Closure Compiler не разобрался, какой символ куда идет, и некорректно сжал код.
Это похоже на недоработку создателей компилятора, но нам-то хочется не просто чтобы работало. Хочется удаление недостижимого кода и другие фичи.

Поэтому более эффективен будет простой стиль объявления, без внешней функции:

// переименовать document в doc
// для лучшего сжатия
var doc = window.document

// пространство имен 
var MyFramework = { }

// нужная функция
MyFramework.used = function() { 
	doc.createElement('div')
}

// ненужная функция
MyFramework.unused = function() {
	alert("unused")
}

// использование
MyFramework.used()

Компиляция этого кода в обычном режиме не даст особых сюрпризов. А вот продвинутый режим, напротив, очень удивит:

var a = window.document;
(function() {
  a.createElement("div")
})();

Google Closure Compiler не только разобрался в структуре и удалил лишний метод - он заинлайнил функции, чтобы итоговый размер получился минимальным.

Как говорится, преимущества налицо.

Продвинутый режим оптимизации сжимает все свойства и методы, кроме находящихся в списке экстернов.

Это является принципиальным отличием и улучшением, по сравнению с другими упаковщиками.

Кроме того, отказ от сохранения внешней ссылочной целостности позволяет заинлайнить константы и функции, вырезать ненужный код и произвести ряд других полезных оптимизаций.

Вырезание невызываемого (и не экспортированного) кода особенно эффективно с большой библиотекой javascript, т.к. с высокой точностью, на уровне переменной/функции, выбрасывает неиспользуемые возможности.

Конечно, для того, чтобы это вырезание работало, библиотека должна быть написана в правильном стиле, с пониманием процесса оптимизации. Примером такой библиотеки является Google Closure Library.

В крупных проектах, оно отлично работает в сочетании системой зависимостей.

Для того, чтобы сделать символ доступным снаружи, используется экспорт.

Для того, чтобы не переименовывать обращение к внешнему символу, он помещается в список экстернов.

Вообще, судя по виду javascript'ов на сайтах, созданных Google, сам Google жмет свои скрипты именно продвинутым режимом оптимизации.


Автор: Гость (не зарегистрирован), дата: 29 мая, 2010 - 00:00
#permalink

На все this, используемые для доступа к свойствам, компилятор выдаёт предупреждения:
JSC_USED_GLOBAL_THIS: dangerous use of the global this object at line ... character ...
Это страшно?

Выдаёт такое предупреждение на

this.elem = elem

в этом примере из статьи:

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

Автор: KOLANICH, дата: 21 июня, 2010 - 11:05
#permalink

если ваш скрипт юзает какую-либо библиотеку, то не надо отдельно создавать файл екстернов
просто укажите файл библиотеки


Автор: Гость (не зарегистрирован), дата: 27 июня, 2011 - 18:31
#permalink

как объявить екстерны внутри кода (не через внешний файл) ?


Автор: C`est la vie (не зарегистрирован), дата: 5 октября, 2011 - 02:43
#permalink

Спасибо за труд по написанию данного цикла статей! Крутая штука этот GCC - действительно, по сравнению с YUI Compressor`ом - как автомобиль в сравнении с великом!
Вот только есть небольшое нарекание...

Была задача объявить переменную в глобальном контексте так, что бы она стала доступной извне. когда пишу:

var myVariable = (function(){ return {}; })();

GCC её сжимает. Когда прописываю в экстерны

var myVariable = {};

GCC выдаёт ошибку: "ERROR - Variable myVariable first declared in [путь к файлу экстернов]".
Сделал пока вместо декларации объявление свойства объекта window:

window['myVariable'] = (function(){ return {}; })();

, но в итоге в сжатом варианте появляется лишнее "window.myVariable" вместо выглядящего более коротко "var myVariable", что слегка напрягает...

Так же напрягает похожая проблема с объектом event, который доступен в глобальном контексте в IE при наступлении любого события - его тоже приходится писать с приставкой "window." - только тогда GCC его понимает и не сжимает. В итоге потом в скомпрессированном скрипте приходится руками выискивать и вычищать эти приставочки "window.", поскольку regexp`ам такое деликатное дело доверять пока боюсь...


Автор: kidar2, дата: 17 апреля, 2012 - 13:03
#permalink

Компиля конечно хорош, но менят стиль кодирования к примеру в уже существующем большом проекте довольно сложно из-за такого подхода.
Почему бы не сделать какие нибудь директивы компиляции? Что-то типа такого
#Ifdef
#endif

Чтобы для определённых участков кода, не делать такое сжатие.


Автор: Гость (не зарегистрирован), дата: 25 марта, 2014 - 10:09
#permalink

function my(elem){
return 0;
}
window['my']=my

На выходе:
window.my=function(){return 0};
Грамотно оптимизирует. Спасибо.


Автор: slitherio (не зарегистрирован), дата: 9 июля, 2021 - 06:39
#permalink

если ваш скрипт юзает какую-либо библиотеку, то не надо отдельно создавать файл екстернов. Так же напрягает похожая проблема с объектом event, который доступен в глобальном контексте в IE при наступлении любого события - его тоже приходится писать с приставкой "window." - только тогда GCC его понимает и не сжимает. В итоге потом в скомпрессированном скрипте приходится руками выискивать и вычищать эти приставочки "window."
slitherio


 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние темы на форуме
Forum