Россия, Сочи, РГПУ им. А.И.Герцена, 1997 |
Оптимизация 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); } })(); }
Советы и замечания
- Этот шаблон влечет много накладных расходов (на смену контекста исполнения на интерфейс веб-браузера и обратно), поэтому общее время выполнения задачи может быть намного больше, чем если запустить ее вычисление в обычном режиме.
- Чем короче каждый цикл, тем больше накладные расходы, тем более интерактивен интерфейс пользователя (он лучше реагирует на действия пользователя), но тем больше общее время выполнения скрипта.
- Если есть уверенность, что каждая итерация алгоритма занимает совсем немного времени (скажем, 10 мс), тогда можно сгруппировать несколько итераций в одну группу, чтобы уменьшить издержки. Решение, начинать ли новый цикл (прерывать текущий) или сделать еще одну итерацию, должно приниматься на основе того, как долго выполняется весь цикл.
- Никогда не передавайте строку в setTimeout! Если передать строку, то браузер будет каждый раз выполнять дополнительный eval при ее запуске, что, в общем случае, довольно сильно увеличит суммарное время выполнения скрипта за счет ненужных вычислений.
- При использовании глобальных переменных в вычислениях перед выходом из очередного цикла убедитесь, что все необходимые данные синхронизированы, чтобы любой другой 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) ); }
При проведении замеров времени можно увидеть следующую картину: