Примечание: ниже приведен перевод заметки из блога разработчика 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); } })(); }
Здесь выложен пример, который демонстрирует сортировку очень большого массива с использованием этого шаблона.
setTimeout
! Если передать строку, то браузер будет каждый раз выполнять дополнительный eval
при ее запуске, что, в общем счете, довольно сильно увеличит общее время выполнения скрипта за счет ненужных вычислений.Мы можете, в конце концов, выполнять все вычисления такого рода на сервере (хотя вам придется иметь дело с преобразованием данных из одной формы в другую и сетевыми задержками, особенно, если объем данных достаточно велик). Запуск «тяжелых» вычислений на клиенте, скорее всего, является признаком глубоких, серьезных архитектурных проблем в вашем приложении.