Статьи

Перевод: Николай Мациевский aka sunnybear
Опубликована: 2 февраля 2008

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

Примечание: ниже приведен перевод заметки из блога разработчика YUI-утилит Julien Lecomte "Running CPU Intensive JavaScript Computations in a Web Browser", в которой автор рассматривает выполнение «тяжелых» вычислений в веб-браузере и приводят ряд методов для их «оптимизации». Мои комментарии даны курсивом.

Введение

Шаблон, который я хочу ниже обсудить, хорошо известен и используется уже более 10 лет. Целью данной заметки является представить этот шаблон в новом свете и, что более важно, обсудить возможные пути для уменьшения накладных расходов.

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

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

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

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

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

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

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

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

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

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

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

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

Заключение

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

Читать дальше

Все комментарии (habrahabr.ru)