Глава 7. Оптимизация JavaScript

7.1. Кроссбраузерный window.onload

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

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

Firefox впереди планеты всей

В Firefox есть событие специально для этих целей: DOMContentLoaded. Следующий образец кода выполняет как раз то, что нам нужно, в Mozilla-подобных браузерах (а также в Opera 9 и старше):

// для Firefox

if (document.addEventListener) {

document.addEventListener("DOMContentLoaded", init, false);

}

А Internet Explorer?

IE поддерживает замечательный атрибут для тега

Содержание этого внешнего скрипта будет состоять только из одной строчки кода:

init();

Условные комментарии

С этим подходом есть некоторая проблема. Другие браузеры проигнорируют атрибут defer и загрузят этот скрипт сразу же. Существует несколько способов, как можно с этим побороться. Можно воспользоваться условными комментариями, чтобы скрыть «отложенный» скрипт:

IE также поддерживает условную компиляцию. Следующий код будет JavaScript-эквивалентом для заявленного выше HTML-кода:

// для Internet Explorer

/*@cc_on @*/

/*@if (@_win32)

document.write("

Javascript — это расширение

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

Это не означает, что мы совсем не можем использовать Javascript, это лишь значит, что мы можем добавлять его только как дополнительную возможность. Страницы должны работать и при выключенном JavaScript — это одно из основных правил «ненавязчивого» JavaScript.

Давайте рассмотрим для примера следующий HTML-код:

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

Мы можем исправить этот фрагмент, заменив button на submit и добавив обработчик события submit для формы:

...

Доверять, но проверять

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

В качестве примера правильных проверок можно привести следующий код:

function color(object, color) {

if(object) {

if (color) {

if (object.style) {

object.style.color = color;

}

}

}

}

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

if (document.getElementById) {

}

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

Доступ к элементам

Каждый XML- (а также и HTML-) документ — это дерево узлов. Узел — часть этого дерева (в качестве аналогии можно привести дерево файлов и директорий на жестком диске). Давайте посмотрим, какие функции и атрибуты мы можем использовать, чтобы перемещаться по дереву документа и выбирать необходимые нам узлы.

getElementById('elementID')

возвращает элемент с идентификатором, равным elementID

getElementsByTagName('tag')

возвращает массив элементов с именем tag

Естественно, мы можем смешивать и сочетать эти два варианта. Несколько примеров:

document.getElementById('nav').getElementsByTagName('a')[1];

//возвращает вторую ссылку внутри элемента, который имеет ID 'nav'

document.getElementsByTagName('div')[1].getElementsByTagName('p')[3];

//возвращает четвертый параграф внутри второго div в документе.

Полный перечень всех DOM-методов, которые поддерживаются сейчас практически всеми браузерами, здесь приводить не имеет смысла. При желании с ними можно ознакомиться на сайте w3.org.

Полезные советы

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

Прежде чем обращаться к элементу, нужно убедиться, что он существует.

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

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

Следует избегать частого перебора элементов. Каждая операция (особенно getElementesByTagName) довольно ресурсоемка. Стоит кэшировать наиболее часто используемые элементы (подробнее о кэшировании в JavaScript рассказывается чуть далее в этой главе).

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

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

Добавляем обработчики событий

Главная техника, которую мы используем, чтобы сохранить наш JavaScript «ненавязчивым», — это хранение скрипта в отдельном файле, что предпочтительней, чем смешивать его с разметкой страницы. Чтобы исполнить функции в нашем .js-файле, нам надо вызвать их, когда страница загружена (подробнее о событии загрузки страницы было рассказано в начале этой главы).

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

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

function addEvent(object, eventType, function){

if (object.addEventListener){

object.addEventListener(eventType, function, false);

return true;

} else {

if (object.attachEvent){

var r = object.attachEvent(\"on\"+eventType, function);

return r;

} else {

return false;

}

}

}

События — довольно сложная тема в Javascript. Для разработки простых веб-сайтов указанных примеров достаточно, но если мы переходим к разработке веб-приложений, тут ситуация многократно усложняется. Поэтому стоит быть внимательным к их функционированию в условиях отключенного или неподдерживаемого JavaScript.

Ускоряем обработку событий

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

Предположим, что у нас основная навигация по сайту включает шесть ссылок сверху, четырем из которых требуются обработчики событий, чтобы поменять у них атрибут href. У этих четырех ссылок атрибут class выставлен в bundle.

Скорее всего, ситуация будет выглядеть следующим образом.

var MenuNavigation = {

init: function() {

var navigation = document.getElementById('mainNav');

var links = navigation.getElementsByTagName('a');

for ( var i = 0, j = links.length; i < j; ++i ) {

if ( /bundle/i.test(links[i].className) ) {

links[i].onclick = this.onclick;

}

}

},

onclick: function() {

this.href = this.href + '?name=value';

return true;

}

}

В этом фрагменте довольно много лишнего. Во-первых, метод getElementsByTagName просматривает каждый дочерний DOM-узел в элементе mainNav, чтобы найти все ссылки. Затем мы еще раз пробегаем по всему найденному массиву, чтобы проверить имя класса каждой ссылки. Это пустая трата процессорного времени на каждом этапе. И это замедление загрузки страницы на уровне 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):

7.3. Применение «ненавязчивого» JavaScript

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

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

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

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

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

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

document.write против innerHTML

Контекстная реклама, пожалуй, является одним из главных «тормозов» при загрузке страницы (при прочих равных условиях), ибо активно применяет document.write, который «морозит» загрузку до получения всех необходимых JavaScript-файлов с внешних серверов. Естественным предположением было бы использовать вместо него 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 файла (отвечающий за логику и содержащий сами объявления), что также добавляет чести данной системе контекстных объявлений.

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

TopLine, Pop-Up, Pop-Under и RichMedia

В стандартных рекламных сетях сейчас превалируют три формата показа объявлений на странице: TopLine, Pop-Under и RichMedia. Последние два весьма дружественны к техникам «ненавязчивого», ибо подключаются только после полной загрузки страницы (хотя такая реклама, возможно, будет слишком раздражающей, чтобы использовать ее на нормальных сайтах). TopLine отличается тем, что должен быть вставлен в самом начале HTML-документа и, таким образом, максимально замедлит его загрузку.

Поскольку TopLine мало чем отличается от стандартных баннеров, то посетители будут довольно лояльны к его использованию. Однако как же нам исправить ситуацию с замедлением загрузки? Так же, как и для контекстной рекламы: переместить вызов document.write в innerHTML (или в appendChild). Что и было успешно проделано. Исходный код модифицированного варианта слишком простой, чтобы приводить его здесь. Однако стандартный код вызова может быть успешно заменен DOM-эквивалентом, который срабатывал по комбинированному событию window.onload и вставлял в заранее подготовленное место все необходимые элементы.

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

Внутренние рекламные сети

На некоторых веб-страницах, использующих внутренние системы показа рекламы, вставка объявлений выполняется через iframe (в общем случае — наиболее быстрый способ), иногда через document.write (иногда даже каскадный, когда с помощью одного document.write вставляется скрипт, в котором содержится другой и т. д.). Последний способ может достаточно замедлить загрузку страницы, если звеньев в цепочке вставок много или же они расположены на медленных серверах.

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

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

Идеальная архитектура рекламной сети

Исходя из всего вышесказанного, можно представить построение внутренней сети показа рекламных объявлений примерно в следующем виде:

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

Создание каталога рекламных мест. Этот этап тоже обычно проходится, но не всегда явно. Каждый рекламный блок может быть откручен только в нескольких соответствующих местах (например, на странице есть 3 возможных варианта для вывода баннера: 240x240, 240x720 и 120x800). Каждое рекламное место должно быть прикреплено к ряду страниц, на которых оно присутствует.

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

Настройка серверной стороны. На самом деле, вместо клиентской логики должен быть разработан серверный модуль, который обеспечит тот же самый функционал, но без дополнительных запросов к серверу. Ведь все (или почти все) данные, как было упомянуто выше, у нас уже есть — так зачем нам дополнительная нагрузка на пользователя?

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

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

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

Разгоняем счетчики: от мифов к реальности

Давайте рассмотрим теперь, что собой представляет код JavaScript-счетчика. Обычно (в 99% случаев) он «вытаскивает» из клиентского окружения набор параметров: URL текущей страницы; URL страницы, с который перешли на текущую; браузер; ОС и т. д. Затем они все передаются на сервер статистики. Все дополнительные возможности счетчиков связаны с обеспечением максимальной точности передаваемой информации (кроссбраузерность, фактически). Наиболее мощные (Omniture, Google Analytics) используют еще и собственные переменные и события, чтобы усилить маркетинговую составляющую.

Но сейчас речь не об этом. Как собранные на клиенте данные попадают на сервер статистики? Все очень просто: в документе создается уникальный элемент, в URL которого «зашиваются» все необходимые значения (обычно в качестве GET-параметров). URL этот ведет, как можно догадаться, на сервер статистики, где данные кладутся в базу и каким-то образом показываются в администраторском интерфейсе.

Как же создается этот самый «уникальный» элемент? Так сложилось, что наиболее простым транспортным средством для данных стала картинка. Обычный однопиксельный GIF-файл (сейчас, в эпоху CSS-верстки, это, пожалуй, единственное его применение) отдается сервером в ответ на URL с параметрами от клиента.

Разбираем по косточкам

Нам нужно гарантировать загрузку внешнего JavaScript-файла «ненавязчивым» образом, при этом обеспечить запрос на сервер статистики (создание картинки со специальными параметрами). В случае Google Analytics все будет очень тривиально, ибо картинка уже создается через new Image(1,1). Однако большинство счетчиков (Рунета и не только) оперируют document.write, и если такая конструкция отработает после создания основного документа, то браузер просто создаст новый, в который запишет требуемый результат. Для пользователя это выльется в совершенно пустую страницу в браузере.

Основная сложность в переносе скриптов статистики в стадию пост-загрузки (по комбинированному событию window.onload, которое описано в начале главы) заключается как раз в изменении вызова картинки, обеспечивающей сбор статистики, на DOM-методы (это может быть не только new Image, но и appendChild). В качестве примера рассмотрим преобразование скрипта статистики для LiveInternet:

document.write("")

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

new Image(1,1).src='http://counter.yadro.ru/hit;tutu_elec?r"

+escape(document.referrer)+((typeof(screen)=="undefined")?"":";s"

+screen.width+"*"+screen.height+"*"

+(screen.colorDepth?screen.colorDepth:screen.pixelDepth))

+";u"+escape(document.URL)+";"+Math.random()

Таким образом (все приведенные участки кода — это одна строка, разбитая для удобства чтения), мы просто заменили вызов document.write на new Image(). Это поможет в большинстве случаев. Если у вас ситуация не сложнее уже описанной, то следующие абзацы можно смело пропустить.

А если сложнее?

Не все счетчики одинаково просты. Например, для сбора статистики с помощью того же Google Analytics нам нужно загрузить целую библиотеку — файл urchin.js или ga.js. На наше счастье, конкретно в этом скрипте данные уже собираются с помощью создания динамической картинки.

Поэтому все, что нам требуется в том случае, если во внешней библиотеке находится мешающий нам вызов document.write, — это заменить его соответствующим образом. Обычно для этого необходимо изменить сам JavaScript-файл. Не будем далеко ходить за материалом и рассмотрим преобразования на примере Omniture — довольно популярной на Западе библиотеки для сбора статистики.

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

var s_code=s.t();if(s_code)document.write(s_code)

В коде Omniture достаточно найти соответствующий return:

return ''

и заменить его на следующий код (заметим, что для src картинки берется переменная rs):

return 'new Image(1,1).src=\"'+rs+'\"'

Затем мы уже можем заменить вызов и в самом HTML-файле на

var s_code=s.t();if(s_code)eval(s_code)

Для того чтобы все окончательно заработало, необходимо заменить в файле s_code.js и остальные вызовы document.write (всего их там два). Выглядит это примерно так:

var c=s.t();if(c)s.d.write(c);

...

s.d.write('

+"'\" height=1 width=1 border=0 alt=\"\">');

меняем на

var c=s.t();if(c)eval(c);

...

new Image(1,1).name=imn;

Внимательные читатели уже заметили, что альтернативой document.write в нашем случае стал eval, что по большому счету не очень хорошо. Однако здесь не ставится задачи перебирать конкретный скрипт «по косточкам», чтобы избавиться от такого костыля. В некоторых случаях стоит ограничиться просто уверенностью, что вся остальная логика останется нетронутой после вмешательств, ибо все изменения касались только отправки собираемых данных на сервер.

Делаем статистику динамической

Итак, мы узнали, как подготовить внешний JavaScript-файл к динамической загрузке. Осталось понять, как теперь это использовать.

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

В той ситуации, когда скрипт совсем внешний (Google Analytics), у нас по большому счету только 2 выхода:

Перенести сам скрипт на наш сервер, добавить в него необходимые инициализационные переменные и вызов (помимо самого объявления) функции статистики (для Google Analytics это urchinTracker()). В качестве плюсов можно отметить то, что в общем случае скрипт будет загружаться с нашего сервера побыстрее, чем будет устанавливаться новое соединение с www.google-analytics.com и проверяться, что файл не изменился. В качестве минусов — необходимость отслеживать (возможные) изменения скрипта и необходимость отдавать JavaScript-файл с собственного сервера со всеми вытекающими из этого последствиями.

Проверять через определенные промежутки времени, загрузилась ли библиотека. Пишется очень простой код, который через каждый 10 мс проверяет, доступна ли из библиотеки необходимая функция. Если да, то она вызывается. В противном случае проверка запускается снова через 10 мс. Плюсы: можно использовать тот же самый скрипт, что и раньше. Минусы: дополнительная (небольшая) нагрузка на клиентский браузер при загрузке. В качестве примера можно рассмотреть следующий код для Goole Analytics:

var _counter_timer = setInterval(function() {

if (urchinTracker) {

urchinTracker();

clearInterval(_counter_timer);

}

}, 10);

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

7.4. Замыкания и утечки памяти

В этом разделе речь идет преимущественно об Internet Explorer и его скриптовом движке — JScript. Однако, во-первых, многие из приведенных методик и советов имеют большое значение для других браузеров и их виртуальных JavaScript-машин. Во-вторых, IE на данный момент занимает порядка 60% пользовательской аудитории, поэтому при рассмотрении эффективного программирования на JavaScript выбрасывать его из поля зрения было бы по меньшей мере глупо.

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

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

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

Шаблоны утечек

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

Основные виды утечек можно разбить на следующие 4 типа.

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

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

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

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

Циклические ссылки

Циклические ссылки являются источником практически любой утечки. Обычно скриптовые движки нормально отрабатывают с циклическими ссылками при помощи собственных сборщиков мусора, однако из-за некоторых неопределенностей их механизм эвристических правил может дать сбой. Одной из таких неопределенностей будет состояние DOM-объекта, к которому имеет доступ текущая порция скрипта. Основной принцип в данном случае можно описать так:

Рис. 7.3. Основной шаблон циклической ссылки

Утечка в таком шаблоне происходит из-за особенностей учета DOM-ссылок. Объекты скриптового движка удерживают ссылку на DOM-элемент и ожидают, пока будут освобождены все внешние ссылки, чтобы освободить, в свою очередь, этот указатель на DOM-элемент. В нашем случае у нас две ссылки на объект скрипта: внутри области видимости скриптового движка и от расширенного свойства DOM-элемента. По окончанию своей работы скрипт освободит первую ссылку, но ссылка из DOM-элемента никогда не будет освобождена, потому что ждет, что это сделает объект скрипта!

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

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

Чтобы разрушить этот шаблон, можно использовать явное присвоение null тому свойству, которое «течет». Таким образом, при закрытии документа мы сообщаем скриптовому движку, что между DOM-элементом и глобальной переменной нет больше никакой связи. В результате все ссылки будут очищены, и сам DOM-элемент будет освобожден. В таком случае веб-разработчик знает больше о внутренних отношениях между объектами, чем сам скрипт, и может поделиться этой информацией со скриптом.

Более сложный случай

Хотя это только базовый шаблон, для более сложных ситуаций может оказаться нелегко выяснить первопричину утечки. Распространенной практикой по написанию объектно-ориентированного JScript является расширение DOM-элементов путем инкапсуляции их внутри JScript-объекта. В процессе создания такого объекта в большинстве случаев получается ссылка на желаемый DOM-элемент, а затем она сохраняется в только что созданном объекте, при этом экземпляр этого объекта оказывается прикрепленным к DOM-элементу. Таким способом модель приложения всегда получает доступ ко всему что нужно. Проблема заключается в том, что это явная циклическая ссылка, но из-за использования других аспектов языка она может остаться незамеченной. Устранение шаблонов такого рода может быть весьма затруднительным, но вы вполне можете использовать простые методы, обсужденные ранее.

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

Замыкания

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

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

Рис. 7.4. Циклические ссылки с самозамыканием

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

В качестве примера можно рассмотреть Объект 2, который был бы освобожден по окончанию вызова функции, в общем случае. Однако после добавления замыкания была создана вторая ссылка на этот параметр, которая не может быть освобождена, пока не будет закрыто замыкание. Если вы прикрепили замыкание к событию, то вам придется его у события в конце концов убрать. Если замыкание прикреплено к расширенному параметру, нужно будет его занулить (приравнять этот параметр к null).

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

Устранить эту утечку не так просто, как в случае с обычной циклической ссылкой. «Замыкание» можно рассматривать как временный объект, который существует в области видимости функции. После завершения функции ссылка на само замыкание теряется, поэтому встает вопрос: как же вызвать завершающий detachEvent?

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

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

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

Постраничные утечки

Утечки, которые зависят от порядка добавления элементов в DOM-дереве, всегда вызваны тем, что создаются промежуточные объекты, которые затем не удаляются должным образом. Это происходит и в случае создания динамических элементов, которые затем присоединяются к DOM. Базовый шаблон заключается во временном соединении двух только что созданных элементов, из-за чего возникает область видимости, в которой определен дочерний элемент. Затем, при включении этого дерева из двух элементов в основное, они оба наследуют контекст всего документа, и происходит утечка во временном объекте (чей контекст не был закрыт).

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

Рис. 7.5. Утечки, связанные с порядком добавления DOM-элементов

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

Запуская пример и возвращаясь к пустой странице, можно замерить разницу в объеме памяти между этими двумя случаями. При использовании первой DOM-модели для прикрепления дочернего узла к родительскому, а затем родительского — к общему дереву, нагрузка на память немного возрастает. Данная утечка использования перекрестных ссылок характерна для Internet Explorer. При этом память не высвобождается, если мы перезапустим IE-процесс. Если протестировать пример, используя вторую DOM-модель для тех же самых действий, то никакого изменения в размере памяти не последует. Таким образом, можно исправить утечки такого рода:

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

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

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

Псевдо-утечки

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

Так же как и ситуация, связанная с добавлением элементов в DOM-дерево, эта проблема опирается на создание временных объектов и приводит к «съеданию» памяти. Переписывая текстовый узел внутри скриптового элемента раз за разом, можно наблюдать, как количество доступной памяти мало-помалу уменьшается из-за различных объектов внутреннего движка, которые были привязаны к предыдущему содержанию. В частности, позади остаются объекты, отвечающие за отладку скриптов, поскольку они полностью принадлежат предыдущему куску кода.

Если мы запустим приведенный код и посмотрим в Диспетчере Задач, что происходит при переходе с «текущей» страницы на чистую, то не увидим никаких утечек. Скрипт расходует память только внутри текущей страницы, и при перемещении на новую вся задействованная память разом освобождается. Ошибка заключается в неверном ожидании определенного поведения. Казалось бы, что переписывание некоторого скрипта приведет к тому, что предыдущий кусок будет бесследно исчезать, оставляя только дополнительные циклические ссылки или замыкания, однако фактически он не исчезает. Очевидно, это псевдо-утечка. В данном случае размер выделенной памяти выглядит устрашающе, но для этого имеется совершенно законная причина.

Проектируем утечки

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

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

7.5. Оптимизируем «тяжелые» JavaScript-вычисления

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

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

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

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);

}

})();

}

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

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

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

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

Никогда не передавайте строку в setTimeout! Если передать строку, то браузер будет каждый раз выполнять дополнительный eval при ее запуске, что, в общем случае, довольно сильно увеличит суммарное время выполнения скрипта за счет ненужных вычислений.

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

Заключение

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

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

7.6. Быстрый 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).

Браузер

Нормальный

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

Таблица 7.2. Сравнение методов работы с DOM-деревом, результаты в миллисекундах

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

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

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++] = "";

for (i = 0; i < 1000; i++) {

html[idx++] = "

";

for (j = 0; j < 5; j++) {

html[idx++] = "

";

}

html[idx++] = "

";

}

html[idx++] = "

";

el = document.createElement("div");

document.body.appendChild(el);

el.innerHTML = html.join("");

7.7. Кэширование в 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, но увеличивает расходование памяти (и может привести к псевдо-утечкам), поэтому в каждом конкретном случае нужно выбирать меньшее из двух зол.

7.8. Быстрые итераторы, регулярные выражения и другие вкусности

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

Итераторы

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

В результате мы получим примерно следующую таблицу (табл. 7.3).

Браузер

Обычный

С кэшем

for-in

Обратный

do-while

Обратный while

Firefox 3.0.3

714

657

835

280

297

217

Safari 3.1.2

141

140

157

125

125

93

Opera 9.61

188

125

765

94

94

78

IE 6

1281

1219

1094

468

500

360

IE 7

1391

1297

1250

515

532

406

IE 8b2

954

906

922

406

422

328

Chrome 0.2

288

246

332

117

114

95

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

В общем случае применение обратного while для перебора цикла в 2–3 раза быстрее всех остальных вариантов. Если веб-приложение оперирует массивами порядка 1000 элементов, то в результате применения оптимизированных приемов будет заметен значительный прирост производительности.

Регулярные выражения

В JavaScript есть несколько способов проверить, удовлетворяет ли строка заданному шаблону:

// 1. Объявляем объект в виде регулярного выражения

var RegExp = '/script/gi';

// и ищем в элементе массива совпадение с заданным шаблоном

items[i].nodeName.search(RegExp);

// 2. можно просто проверять соответствие строке,

// а не искать индекс подстроки

items[i].nodeName.match(RegExp);

// 3. Можно обойтись без объявления самого регулярного выражения

items[i].nodeName.match(/script/gi);

// 4. Можно задавать регулярное выражение без глобального модификатора,

// ведь мы ищем любое (=первое) совпадение шаблона

items[i].nodeName.match(/script/i);

// 5. С тем же успехом мы можем выполнить шаблон

/script/i.exec(items[i].nodeName);

// 6. Наконец, можно протестировать сам шаблон на нахождение в строке

/script/i.test(items[i].nodeName);

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

Браузер

search

match

«На лету»

Локальный

exec

test

Firefox 3.0.3

2120

2041

1295

1273

1225

1348

Safari 3.1.2

453

469

344

359

360

359

Opera 9.61

2141

2063

406

344

312

313

IE 6

2594

2516

1875

1859

1953

1906

IE 7

2562

2469

1859

1844

2000

1860

IE 8b2

2140

2032

1453

1453

1547

1469

Chrome 0.2

856

870

416

397

385

392

Таблица 7.4. Различные варианты выполнения регулярного выражения, результаты в миллисекундах

Как мы видим, в данном случае создание нового регулярного выражения — весьма ресурсоемкий процесс, поэтому в большинстве случаев лучше обходиться без него. В остальном все браузеры ведут себя достаточно ровно при сопоставлении match, exec и test.

Загрузка...