Опубликован: 16.02.2009 | Уровень: специалист | Доступ: платный
Лекция 7:

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

А если еще быстрее?

Давайте подумаем еще немного. Зачем нам каждый раз создавать фрагмент документа, если мы для этой цели можем использовать обычный его узел (фактически создавать кэш нашего узла, который мы собираемся везде менять)? Так можно прийти к следующему фрагменту кода:

var div = document.getElementsByTagName("div");
var child = document.createElement("div");
var parent = div[0].parentNode;

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

for ( var i = 0; i < div.length; i++ ) {
// для IE
	if (IE) {
		parent.replaceChild(child.cloneNode(true),div[i]);
// для других браузеров
	} else {
		div[i] = child.cloneNode(true);
	}
}

В нем соответствующие узлы документа заменяются на клонированный вариант кэшированной версии (без создания DocumentFragemnt ). Это работает еще быстрее (везде, кроме IE - примерно на порядок, в IE - в полтора-два раза).

innerHTML нам поможет

Чтобы уменьшить отрисовку отдельных частей документа в процессе добавления какого-либо большого фрагмента, мы можем сохранять HTML-код в виде текста и лишь на финальном этапе вставлять его в DOM-дерево. Давайте рассмотрим следующий пример:

var i, j, el, table, tbody, row, cell;
el = document.createElement("div");
document.body.appendChild(el);
table = document.createElement("table");
el.appendChild(table);
tbody = document.createElement("tbody");
table.appendChild(tbody);
for (i = 0; i < 1000; i++) {
	row = document.createElement("tr");
	for (j = 0; j < 5; j++) {
		cell = document.createElement("td");
		row.appendChild(cell);
	}
	tbody.appendChild(row);
}

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

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

var i, j, el, idx, html;
idx = 0;
html = [];
html[idx++] = "<table>";
for (i = 0; i < 1000; i++) {
	html[idx++] = "<tr>";
	for (j = 0; j < 5; j++) {
		html[idx++] = "<td></td>";
	}
	html[idx++] = "</tr>";
}
html[idx++] = "</table>";
el = document.createElement("div");
document.body.appendChild(el);
el.innerHTML = html.join("");

Кэширование в JavaScript

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

Итерации и локальное кэширование

При DOM-операциях перебор массива объектов является довольно типичной задачей. Давайте предположим, что вы разрабатываете HTML-приложение, которое индексирует содержание страниц. Нашей задачей является сбор всех элементов h1 на текущей странице, чтобы затем использовать их в проиндексированном массиве.

Ниже приведен пример того, как это можно осуществить:

function Iterate(aEntries) {
	for (var i=0; i < document.getElementsByTagName('h1').length; i++) {
		aEntries[aEntries.length] =
			document.getElementsByTagName('h1')[i].innerText;
	}
}

Что плохого в приведенном примере? Он содержит два обращения к массиву document.getElementsByTagName('h1') на каждой итерации. Внутри цикла наш скрипт будет:

  • вычислять размер массива;
  • получать значение свойства innerText для текущего элемента в массиве.

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

function Iterate2(aEntries) {
	var oH1 = document.getElementsByTagName('h1');
	var iLength = oH1.length;
	for (var i=0; i < iLength; i++) {
		aEntries[aEntries.length] = oH1(i).innerText;
	}
}

Таким образом, мы кэшируем DOM-массив в локальную переменную, и затем все действия над ней производятся гораздо быстрее. N обращений к DOM-дереву превращается всего в одно-единственное в результате использования кэширования.

Кэширование ресурсоемких вызовов

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

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

Если у вас есть примерно такой участок кода:

var arr = ...;
var globalVar = 0;
(function () {
	var i;
	for (i = 0; i < arr.length; i++) {
		globalVar++;
	}
})();

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

var arr = ...;
var globalVar = 0;
(function () {
	var i, l, localVar;
	l = arr.length;
	localVar = globalVar;
	for (i = 0; i < l; i++) {
		localVar++;
	}
	globalVar = localVar;
})();

В этом примере мы уменьшили число обращений к глобальной переменной и устранили расчет размера массива на каждой итерации цикла.

Кэшируем цепочки вызовов

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

for (i=0; i < 10000; i++) a.b.c.d(v);

то он будет выполняться несколько медленнее, чем

var f=a.b.c.d;
for (i=0; i < 10000; i++) f(v);

или

var f=a.b.c;
for (i=0; i < 10000; i++) f.d(v);

На данный момент браузеры хорошо справляются с кэшированием вызовов функций, и особого прироста производительности при такой оптимизации в IE и Firefox не наблюдается. При проектировании приложений для других браузеров стоит учитывать и этот аспект, однако с большой вероятностью кэширование вызовов объектов уже добавлено (или будет добавлено в самое ближайшее время) в разрабатываемые JavaScript-движки, ибо это одно из наиболее узких мест в производительности веб-приложений.

Наконец, при использовании кэширования (особенно частей DOM-дерева) стоит помнить, что оно уменьшает использование CPU, но увеличивает расходование памяти (и может привести к псевдо-утечкам), поэтому в каждом конкретном случае нужно выбирать меньшее из двух зол.

Дарья Билялова
Дарья Билялова
Россия
Елена Петрушевская
Елена Петрушевская
Россия, г. Нижневартовск