Статьи

Автор: Evgeniy Kucheruk
Опубликована: 22 января 2007, kuklaora.blogspot.com

Рекомендации по быстродействию в IE + JScript, часть третья: проблемные места JS

Оригинал: IE+JScript Performance Recommendations Part 3: JavaScript Code Inefficiencies

Примечание: Переводчик из меня аховый, любые комментарии\исправления очень приветствуются.

Снова привет, я, Питер Гуревич (Peter Gurevich), менеджер по оптимизации в IE. Мы получили множество отзывов к нашим первым заметкам по быстродействию в IE + JScript (Часть 1, Часть2 (перевод)), и мне не терпиться узнать, что вы думаете про третью часть.

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

Замыкания (ссылки на функции с контекстом) — как очень мощный, так и очень опасный инструмент. Их используют в нескольких техниках программирования, и как правило, сильно ими злоупотребляют. На текущий момент проблема замыканий, которые используются с объектами DOM и компонентами COM, — значительное увеличение использования памяти браузером. Это статья посвящена, прежде всего, быстродействию, поэтому стоит помнить, что в некоторых случаях гораздо лучше с точки зрения производительности использовать обычные функции.

Чаще всего замыкания используются как обработчики событий

Это делается для того, чтобы инкапсулировать область видимости выполняемой функции в новую функцию, которая исполнится при вызове соответствующего обработчика события. Проблема этого подхода — циклические ссылки между переменными и замыканием почти не заметны. Дополнительная нагрузка на память в создании таких объектов вызывает дополнительные расходы на сборку мусора, возможно, дополнительную работу для IE или других браузеров. В качестве хорошего примера можно рассмотреть API для получения данных с удаленного узла.

function startDownload(url)
{
    var source = new ActiveXObject("Ficitious.UrlFetcher");
    source.ondataready = new function() {
	source.ondataready = null;
    }
    source.startRequest(url);
}

Это очень простой пример. Скрипт не делает никакой реальной работы, но представляет пример использования замыкания. Что произойдет, если событие ondataready никогда не будет вызвано? Мы ожидаем, что должно, но ведь всякое бывает. Если оно не сработает, замыкание будет ссылаться на объект, а объект на замыкание. Получится сложная для выявления циклическая ссылка, и у IE начнутся утечки памяти. Это всегда плохо для быстродействия.

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

Каким образом убрать из кода замыкание, зависит от конкретного случая. Если у вас используется только один экземпляр класса, рекомендуем воспользоваться единственным глобальной функцией. Если вы используете множественные запросы — возможно, вам подойдет организация пула в совокупности с диспетчером, обрабатывающим все входящие соединения по сигналу «готово» от запроса. Для дополнительной информации о замыканиях смотрите раздел «Замыкания» в статье Практический JS: избавляемся от утечек памяти в IE

Не используйте методы доступа к свойствам

Часто применяемая техника в ООП подразумевает использование методов для доступа к свойствам. Например, в виде [get/set]_PropertyName (или другие, в завимости от стиля кодирования). Изначально это локальная переменная класса с двумя методами для чтения и записи переменной. Эти методы часто использую для контроля видимости членов класса — но в JScript видимо все. К тому же большинство объектно-ориентированных языков оптимизируют эти свойства до прямого доступа к переменной во время компиляции, а это невозможно в JScript как в интерпретируемом языке.

К примеру, образец объекта Car, использующего средства доступа:

function Car()
{
    this.m_tireSize = 17;
    this.m_maxSpeed = 250; // Чья-то мечта!
    this.GetTireSize = Car_get_tireSize;
    this.SetTireSize = Car_put_tireSize;
function Car_get_tireSize()
{
    return this.m_tireSize;
}
function Car_put_tireSize(value)
{
    this.m_tireSize = value;
}
var ooCar = new Car();
var iTireSize = ooCar.GetTireSize();
ooCar.SetTireSize(iTireSize + 1); // Апгрейд

Этот пример прекрасен с точки зрения ООП, но ужасен для JScript. Окольные пути для доступа к локальным членам класса очень плохо влияют на производительность приложения. Пока нам не нужна проверка входящих значений, ни в коем случае не стоит добавлять лишний код: он должен быть максимально четким.

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

function Car()
{
    this.m_tireSize = 17;
    this.m_maxSpeed = 250; // Чья-то мечта!
}
var perfCar = new Car();
var iTireSize = perfCar.m_tireSize;
perfCar.m_tireSize = iTireSize + 1; // Апгрейд

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

Для полного примера мы также можем добавить прототипы. Учтите, что работа с прототипом действительно не эффективна, так как сначала поиск члена класса проводиться в экзепляре объекта, а потом уже в прототипе. Все это точно сделает наш пример медленнее. Если вы создаете тысячи экземпляров объекта, тогда прототипы становятся, действительно, эффективными. Это достигается за счет снижения размера каждого объекта, так как дополнительные свойства не добавляются в каждый создаваемый экземпляр. Более того, инициализация объекта также может быть быстрее, так как не потребует каждый раз установки всех функций. Для завершения ниже приводиться полный пример. В качестве домашнего задания попробуйте найти места в которых машина на прототипе выиграет у медленной машины.

<script>
// Объявление медленной машины
function SlowCar(){
    this.m_tireSize = 17;
    this.m_maxSpeed = 250;
    this.GetTireSize = SlowCar_get_tireSize;
    this.SetTireSize = SlowCar_put_tireSize;
}
function SlowCar_get_tireSize()
{
    return this.m_tireSize;
}
function SlowCar_put_tireSize(value)
{
    this.m_tireSize = value;
}</script>

<script>
// Машинка побыстрее, без методов доступа к данным
function FasterCar()
{
    this.m_tireSize = 17;
    this.m_maxSpeed = 250;
}
</script>

<script>
// Машина на прототипах
function PrototypeCar()
{
    this.m_tireSize = 17;
    this.m_maxSpeed = 250;
}

PrototypeCar.prototype.GetTireSize = function() { return this.m_tireSize; };
PrototypeCar.prototype.SetTireSize = function(value) { this.m_tireSize = value; };
</script>

<script>
function TestDrive()
{
    var slowCar = new SlowCar(); // Корректно и безопасно, возможно, небыстро
    var fasterCar = new FasterCar(); // Отсутствуют воздушные подушки, возможно быстрее
    var protoCar = new PrototypeCar(); // Может ли технология выиграть?

    var start = (new Date()).getTime();
    for(var i = 0; i < 100000; i++) { slowCar.SetTireSize(slowCar.GetTireSize() + 1); }
    var end = (new Date()).getTime();
    output.innerHTML += "Медленная машина " + (end - start) + "<br>";

    start = (new Date()).getTime();
    for(var i = 0; i < 100000; i++) { fasterCar.m_tireSize += 1; }
    end = (new Date()).getTime();
    output.innerHTML += "Быстрая машина " + (end - start) + "<br>";

    start = (new Date()).getTime();
    for(var i = 0; i < 100000; i++) { protoCar.SetTireSize(protoCar.GetTireSize() + 1); }
    end = (new Date()).getTime();
    output.innerHTML += "Машина на прототипах " + (end - start) + "<br>";
</script>

<button onclick="TestDrive();">Протестируем машины!</button>
<div id="output"></div>

На этом все с третьей частью, Спасибо.

Peter Gurevich, Program Manager

Justin Rogers, Software Development Engineer

Все комментарии