Опубликован: 16.02.2009 | Доступ: свободный | Студентов: 1426 / 138 | Оценка: 4.26 / 4.17 | Длительность: 16:10:00
ISBN: 978-5-9963-0024-2
Лекция 7:

Оптимизация JavaScript

Оптимизируем вычисления

Google Gears (http://gears.google.com/) обеспечивает выполнение напряженных вычислений без двух вышеоговоренных ограничений. Однако в общем случае нельзя полагаться на наличие Gears (в будущем было бы замечательно, чтобы решение по типу Gears WorkerPool API стало частью стандартного API браузеров).

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

function doSomething (callbackFn [, additional arguments]) {
	// Выполняем инициализацию
	(function () {
		// Делаем вычисления...
		if (конечное условие) {
			// мы закончили
			callbackFn();
		} else {
			// Обрабатываем следующий кусок
	  		setTimeout(arguments.callee, 0);
		}
	})();
}

Улучшаем шаблон

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

function doSomething (progressFn [, дополнительные аргументы]) {
	// Выполняем инициализацию
	(function () {
	// Делаем вычисления...
		if (условие для продолжения) {
		// Уведомляем приложение о текущем прогрессе
			progressFn(значение, всего);
		// Обрабатываем следующий кусок
			setTimeout(arguments.callee, 0);
		}
  	})();
}

Советы и замечания

  1. Этот шаблон влечет много накладных расходов (на смену контекста исполнения на интерфейс веб-браузера и обратно), поэтому общее время выполнения задачи может быть намного больше, чем если запустить ее вычисление в обычном режиме.
  2. Чем короче каждый цикл, тем больше накладные расходы, тем более интерактивен интерфейс пользователя (он лучше реагирует на действия пользователя), но тем больше общее время выполнения скрипта.
  3. Если есть уверенность, что каждая итерация алгоритма занимает совсем немного времени (скажем, 10 мс), тогда можно сгруппировать несколько итераций в одну группу, чтобы уменьшить издержки. Решение, начинать ли новый цикл (прерывать текущий) или сделать еще одну итерацию, должно приниматься на основе того, как долго выполняется весь цикл.
  4. Никогда не передавайте строку в setTimeout! Если передать строку, то браузер будет каждый раз выполнять дополнительный eval при ее запуске, что, в общем случае, довольно сильно увеличит суммарное время выполнения скрипта за счет ненужных вычислений.
  5. При использовании глобальных переменных в вычислениях перед выходом из очередного цикла убедитесь, что все необходимые данные синхронизированы, чтобы любой другой JavaScript-поток, который может быть запущен между двумя циклами, мог их свободно изменить.

Заключение

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

Также в качестве альтернативного варианта можно рассмотреть отправку каких-либо данных на сервер с помощью XHR-запроса, их обработку и отображение на клиенте. Поскольку JavaScript - интерпретируемый язык в браузере, то он выполняется на несколько порядков дольше серверных аналогов.

Быстрый DOM

Работа с DOM-деревом в JavaScript является самым проблематичным местом. Его можно сравнить только разве что с базой данных для серверных приложений. Если JavaScript выполняется очень долго, скорее всего, дело именно в DOM-методах. Ниже рассмотрено несколько прикладных моментов, то есть способов максимально ускорить этот "затор".

DOM DocumentFragment: быстрее быстрого

DocumentFragment является облегченным контейнером для DOM-узлов. Он описан в спецификации DOM1 и поддерживается во всех современных браузерах (был добавлен в Internet Explorer в 6-й версии).

В спецификации говорится, что различные операции - например, добавление узлов как дочерних для другого Node - могут принимать в качестве аргумента объекты DocumentFragment ; в результате этого все дочерние узлы данного DocumentFragment перемещаются в список дочерних узлов текущего узла.

Это означает, что если у нас есть группа DOM-узлов, которые мы добавляем к фрагменту документа, то после этого можно этот фрагмент просто добавить к самому документу (результат будет таким же, если добавить каждый узел к документу в индивидуальном порядке). Тут можно заподозрить возможный выигрыш в производительности. Оказалось, что DocumentFragment также поддерживает метод cloneNode. Это обеспечивает нас полной функциональностью для экстремальной оптимизации процесса добавления узла в DOM-дерево.

Давайте рассмотрим ситуацию, когда у нас есть группа узлов, которую нужно добавить к DOM-дереву документа (в тестовой версии это 12 узлов - 8 на верхнем уровне - против целой кучи div ).

var elems = [
	document.createElement("hr"),
	text( document.createElement("b"), "Links:" ),
	document.createTextNode(" "),
	text( document.createElement("a"), "Link A" ),
	document.createTextNode(" | "),
	text( document.createElement("a"), "Link B" ),
	document.createTextNode(" | "),
	text( document.createElement("a"), "Link C" )
];

function text(node, txt){
	node.appendChild( document.createTextNode(txt) );
	return node;
}

Нормальное добавление

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

var div = document.getElementsByTagName("div");

for ( var i = 0; i < div.length; i++ ) {
	for ( var e = 0; e < elems.length; e++ ) {
		div[i].appendChild( elems[e].cloneNode(true) );
	}
}

Добавление при помощи DocumentFragment

Однако если мы будем использовать DocumentFragment для совершения тех же самых операций, то ситуация изменится. Для начала мы добавим все наши узлы к самому фрагменту (используя имеющийся метод createDocumentFragment ).

Самое интересное начинается тогда, когда мы собираемся добавить сами узлы в документ: нам нужно вызвать по одному разу appendChild и cloneNode для всех узлов!

var div = document.getElementsByTagName("div");
var fragment = document.createDocumentFragment();

for ( var e = 0; e < elems.length; e++ ) {
	fragment.appendChild( elems[e] );
}

for ( var i = 0; i < div.length; i++ ) {
	div[i].appendChild( fragment.cloneNode(true) );
}

При проведении замеров времени можно увидеть следующую картину:

Таблица 7.2. Сравнение методов работы с DOM-деревом (результаты в миллисекундах)
Браузер Нормальный Fragment
Firefox 3.0.1 90 47
Safari 3.1.2 156 44
Opera 9.51 208 95
IE 6 401 140
IE 7 230 61
IE 8b1 120 40