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

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

Немного усложним

Можно прикрепить один-единственный обработчик событий к элементу mainNav, чтобы затем отслеживать все клики на ссылки внутри него:

var MenuNavigation = {
 init: function() {
 	var navigation = document.getElementById('mainNav');
 	navigation.onclick = this.onclick;
 },
 onclick: function(e) {
 	if ( /bundle/i.test(e.target.className) ) {
 		e.target.href = e.target.href + '?name=value';
 	}
 	return true;
 }
}

Простота и элегантность данного подхода должны быть очевидны, но у него есть и некоторое количество преимуществ в плане производительности:

  • Чем меньше приемников событий прикреплено к документу, тем лучше. Они все загружаются в память и в чрезвычайных случаях могут сильно замедлить работу браузеров. Также это увеличивает число замыканий, что чревато утечками памяти. Подробнее рассказывается далее в этой главе.
  • Загружается меньше кода на странице. Одной из главных проблем для сложных веб-приложений является задержка при загрузке JavaScript для исполнения и визуализации документа. Два цикла из первого примера отсутствуют во втором.
  • "Исполнение по требованию". Второй пример выполняет немного больше действий, когда вызывается конечный обработчик событий, но это лучше, чем выполнять все действия при загрузке страницы, когда мы даже не знаем, будет ли запущен каждый конкретный обработчик событий. Ссылок на странице может быть сотня, а пользователь нажмет только одну или две из них.

Боремся с Internet Explorer

Есть одна небольшая проблема при использовании изложенного выше кода. Определение целевого элемента у события, на самом деле, не является просто вызовом e.target. В Internet Explorer необходимо использовать e.srcElement. Самым простым решением для устранения этой проблемы является небольшая функция getEventTarget. Ниже представлена наиболее актуальная версия.

function getEventTarget(e) {
 var e = e || window.event;
 var target = e.target || e.srcElement;
 if (target.nodeType == 3) { // боремся с Safari
 	target = target.parentNode;
 }
 return target;
}

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

Пойдем дальше

А что, если нам нужно добавить такой обработчик на все ссылки (или почти на все)? Правильно: тогда для контейнера всех этих ссылок стоит выбрать document.body. Ниже приведен пример кода, который позволяет так сделать.

var MenuNavigation = {
 init: function() {
 	document.body.onclick = function(e) {
 		var target = getEventTarget(e);
 		if ( target && /bundle/i.test(target.className) ) {
 			target.href += '?name=value';
 		}
 		return true;
 	};
 }
 var getEventTarget = function(e) {
 	var e = e || window.event;
 	var target = e.target || e.srcElement;
// боремся с Safari и вложенностью
 	while ( !target.href || target.nodeType == 3 ) {
 		target = target.parentNode;
 	}();
 	return target;
 }
}
window.onload = MenuNavigation.init;

Если мы собираемся обрабатывать все ссылки, то нужно учесть, что в них могут быть вложены и картинки, и другие теги, поэтому добавлено рекурсивное "всплытие" ссылки: проверяется родитель объекта, на котором сработало событие, и если у него не определен атрибут href, то перебор продолжается, иначе возвращаем искомый объект. Вложение ссылок друг в друга запрещено стандартами, так что если мы сами же проектируем HTML-код, то бояться нечего.

Обработка событий в браузерах

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

node.onclick = function(){

}

Если нужно несколько событий или просто "осторожничаем", то можно воспользоваться следующей распространенной записью:

if (node.addEventListener)
node.addEventListener('click', function(e){}, false);
else
node.attachEvent('onclick', function(){});

Или таким модицифицированным вариантом (меньше символов):

if (node.attachEvent)
	node.attachEvent('onclick', function(){});
else
	node.addEventListener('click', function(e){}, false);

Можно также использовать отдельную переменную для обработчика события:

var addEvent = node.attachEvent || node.addEventListener;
addEvent(/*@cc_on 'on'+@*/'click', function(){}, false);

Или записать в одну строку с использованием условной компиляции:

node[/*@cc_on !@*/0 ? 'attachEvent' : 'addEventListener']
 	(/*@cc_on 'on'+@*/'click', function(){}, false);

Работаем с событиями

Давайте рассмотрим, что мы можем извлечь из события после перехвата его с помощью соответствующего обработчика:

node[/*@cc_on !@*/0 ? 'attachEvent' : 'addEventListener']
 (/*@cc_on 'on'+@*/'click', function(e){

 var target = e.target || e.srcElement

 // или
 if (!e.target) {
 	e.target = e.srcElement
 }

 // или, если нам надо всего один раз
 (e.target || e.srcElement).tagName

 // true везде кроме IE, в котором this === window
 this == node;
	// отменяем всплытие события
	if (e.stopPropagation)
		e.stopPropagation()
	else
		e.cancelBubble
	
	// или просто используем вариант, который
	// для совместимости работает во всех браузерах. 
	e.cancelBubble = true

	// убираем действие по умолчанию (в данном случае клик)
	if (e.preventDefault)
		e.preventDefault()
	else
		e.returnValue = false
	// при attachEvent (как здесь) работает только в IE; 
	// при назначении напрямую (node.onclick) - везде
	return false;
	
}, false):

Применение "ненавязчивого" JavaScript

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

Принципы "ненавязчивой" рекламы

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

Как было продемонстрировано в исследованиях 2007-2008 годов, большая часть задержек при загрузке страницы у обычного пользователя приходится на долю рекламы, подключаемой в основном через JavaScript. Далее будут рассмотрены основные типы использования рекламы на сайтах и предложены способы (в большинстве своем опробованные на практике) для разгона ее загрузки.

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

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

document.write против innerHTML

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

Принцип первый: при проектировании рекламных вызовов используйте innerHTML или script.src (последний подразумевает подключение внешнего JavaScript-файла путем создания соответствующего дополнительного узла в head после загрузки страницы, техника более подробно описана в начале главы). Идеальным является подход, когда для оценки эффективности рекламы не применяется клиентская логика (все показы и переходы отслеживаются по серверным логам).

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

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

Контекстная реклама

Основными игроками на рынке контекстной рекламы на данный момент являются Яндекс.Директ, Google AdSense и Бегун. Google поступает наиболее практично: в результате вызова скрипта вставляется iframe, который дальше уже загружает все рекламное объявление. Поскольку исходные файлы рекламных скриптов расположены на одной из самых доступных и быстрых CDN в мире, то скорость отображения таких объявлений на клиенте впечатляет.

С Яндексом ситуация похуже. Мало того, что выполняется document.write содержимого рекламных объявлений в основное DOM-дерево, к тому же загружается порядка 5 дополнительных файлов в виде узлов текущего документа (в частности, это счетчики и файл стилей). Все это не самым лучшим образом сказывается на быстродействии. Преобразовать в данном случае вызов Яндекс.Директа к виду innerHTML не удается (однако вполне вероятно, что разработчики со стороны Яндекса в ближайшее время изменят JavaScript-код, и такая возможность появится).

Бегун в этом плане приятно удивил: он предоставляет (слабо документированное, но все же) API для множественных вставок рекламных объявлений на страницу при помощи innerHTML. Для этого всего лишь нужно выставить JavaScript-переменные:

begun_multispan=1,begun_spans=[{'span_id':'ad','limit':7,'width':230}]

В данном случае подключается множественный показ рекламных объявлений ( begun_multispan=1 ), далее задается, в каком месте и в каком количестве их показывать. Это происходит через массив begun_spans, где для span_id назначается идентификатор блока, в который будут вставлены объявления после загрузки, limit указывает на их количество в данном блоке, а width просто описывает ширину рекламного блока (в пикселях). Таким образом, можно вставить код Бегуна в самый низ страницы и максимально ускорить ее загрузку.

В случае Бегуна в клиентский браузер в минимальном варианте загружается всего 2 файла (отвечающий за логику и содержащий сами объявления), что также добавляет чести данной системе контекстных объявлений.

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