Примечание: ниже находится перевод презентации от Douglas Crockford JavaScript: The Good Parts. Part Six: Ajax Performance, в которой освещаются фундаментальные проблемы проектирования клиентских веб-приложений и эффективная их оптимизация. Мои комментарии далее курсивом.
Если в руках только молоток, то любая проблема выглядит как веб-страница.
Каждая страница является веб-приложением, которое использует сервер как хранилище данных.
Когда пользователь что-то делает, мы посылаем JSON-запрос на сервер и получаем в ответ JSON-сообщение.
Со стороны сервера JSON-сообщение гораздо легче создать, его быстрее передать по сети, и самому браузеру проще обработать и отобразить такое сообщение, чем HTML-документ.
Рисунок 1. Передача сообщений между браузером и сервером: HTML и JSON
Как приложение распределяется между браузером и сервером?
Ищите что-то среднее, например, элементарный диалог между отдельными частями системы.
Клиент и сервер находятся в постоянном диалоге. Сообщения между ними должны быть максимально короткими (но не стоит забывать об издержках на передачу сообщений, в частности, о среднем заголовке в 500 байтов).
Клиенту не нужна копия базы данных: в каждый конкретный момент ему лишь нужно достаточно информации для обслуживания пользователя.
Но и не стоит переписывать серверное приложение на JavaScript.
Браузер — совсем не эффективная платформа для приложений. Если ваше веб-приложение раздуется, то производительность сильно упадет.
Клиентское программирование должно быть максимально легким.
Хотя и страшно медленно.
В любом случае, это очень сложная платформа для разработок: существует большое количество проблем, связанных как с безопасностью, так и с производительностью.
Браузер не разрабатывался как платформа для приложений (кроме разве что Google Chrome). Да и Ajax очень сильно нагружает браузер.
Оптимизация ничего не стоит, если приложение еще не работает правильно: если что-то происходит не так, не важно, насколько быстро это происходит.
Однако тестировать производительность нужно на ранних стадиях разработки. Причем стоит тестировать в условиях, максимально приближенных к боевым: медленная связь, медленные компьютеры. Локальная сеть и мощные машины веб-разработчиков могут скрыть проблемы производительности.
Donald KnuthПреждевременная оптимизация является корнем всего зла.
Несколько советов для эффективной оптимизации
var fibonacci = function (n) { return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); };
fibonacci(40)
вызовет сама себя 331 160 280 раз.
var memoizer = function (memo, fundamental) { var shell = function (n) { var result = memo[n]; if (typeof result !== 'number') { result = fundamental(shell, n); memo[n] = result; } return result; }; return shell; }; var fibonacci = memoizer([0, 1], function (recur, n) { return recur(n - 1) + recur(n - 2); });
fibonacci(40)
вызовет сама себя 38 раз.
Ключевым аспектом оптимизации является предупреждение возможной работы.
Код высокого качества меньше подвержен проблемам, связанным со специфическими платформами. Также существуют правила кодирования для языка программирования JavaScript: javascript.crockford.com/code.html.
Используйте JSLint для проверки веб-приложения на правильность. Постарайтесь проходить проверку без предупреждений.
Не стоит дожидаться релиза для проведения пересмотра кода. Команда разработчиков должна проводить регулярные проверки в процессе создания приложения.
В процессе пересмотра более опытные разработчики могут обучать новичков или делиться своими наработками. Новички при этом могут обучаться у всей группы.
Любые проблемы (В том числе, и с производительностью) можно обнаружить на ранних стадиях. При этом и качественные наработки получают более широкое распространение.
Все имеет свою цену. Иногда очень большую. Чем больше становится на странице отдельных приложений и виджетов, тем медленнее она начинает работать.
Если разогнать часть программы, которая выполняется малую часть времени, то и общий прирост производительности будет невелик. Если в результате профилирования оказалось, что бОльшая часть времени тратится на A, то не стоит беспокоиться по поводу оптимизации C.
Рисунок 2. Соотношение времени выполнения частей программы
Если бы JavaScript был бесконечно быстрым, то все страницы загружались бы примерно за одно и то же время. Основное узкое место заключается в DOM-методах. Каждая операция с DOM-деревом стоит очень дорого. Каждая такая операция может привести к перерисовке (reflow), которое само по себе недешево.
Быстрее будет оперировать с узлами DOM-дерева до того, как они прикреплены к этому дереву, потому что это предотвращает издержки на перерисовку страницы. Выставление innerHTML
для конкретного узла очень ресурсоемко, но браузеры с этим хорошо справляются, и это позволяет свести все взаимодействия с DOM к одному-единственному.
Эффективное повторное использование уже написанного кода позволит вашим виджетам работать более эффективно.
Рисунок 3. Средние затраты времени для сайтов из Alexa 100
Сокращаем наиболее частые подвыражения (кэшируем) и облегчаем циклы (уменьшаем количество переменных в них). Большинство компиляторов языков программирования уже делают это за вас, но только не JavaScript.
var i; for (i = 0; i < divs.length; i += 1) { divs[i].style.color = "black"; divs[i].style.border = thickness + 'px solid blue'; divs[i].style.backgroundColor = "white"; }
var border = thickness + 'px solid blue', nrDivs = divs.length, ds, i; for (i = 0; i < nrDivs; i += 1) { ds = divs[i].style; ds.color = "black"; ds.border = border; ds.backgroundColor = "white"; }
Конкатенация выполняется при помощи оператора +
. При каждой операции выделяется память: foo = a + b;
. Конкатенацию больших строк лучше выполнять через array.join('')
(работа с массивами также ресурсоемка, поэтому в случае небольшого числа операций выигрыша никакого не будет). Содержимое массива будет объединено в строку следующим образом:
foo = [a, b].join('');// foo === a + b
У некоторых браузеров отдельные операции выполняется очень не эффективно. Однако даже если в браузере A что-то работает быстрее, это может быть медленнее в браузере B (поэтому хорошо тестировать производительность во всех браузерах после применения каких-либо приемов).
Поскольку производительность браузеров следующего поколения может очень сильно отличаться от текущей, то стоит избегать краткосрочных оптимизаций. При оптимизации всегда ориентируйтесь на будущее.
Интуиция очень часто обманывает. Измеряйте производительность следующим образом:
start_time = new Date().valueOf(); code_to_measured(); end_time = new Date().valueOf(); elapsed_time = end_time - start_time;
Измерить время выполнения отдельной операции невозможно. Таймеры в браузерах могут обновляться только раз в 15 мс (в таком случае хорошо прогонять множественные циклы из элементарных операций и замерять общее время выполнения). Однако стоит помнить и о том, что даже точные вычисления могут привести к неверным выводам.
В этом случае операция выполняется только один раз, и особо оптимизировать ее не имеет смысла. Однако если это время инициализации библиотеки, стоит помнить о том, что оно включается во время загрузки каждой страницы сайта.
Стоит оптимизировать операции, которые выполняются многократно. В этом случае производительность будет линейно зависеть от числа запусков.
Рисунок 4. Линейная зависимость времени выполнения от числа итераций
Время выполнения операций может проходить несколько стадий:
Неэффективность
Рисунок 5. Неэффективность для времени выполнения
Дискомфорт
Рисунок 6. Дискомфорт для времени выполнения
Срыв
Рисунок 7. Срыв для времени выполнения
Рисунок 8. Линейно-логарифмическая зависимость времени выполнения от числа итераций
Рисунок 9. Степенная зависимость времени выполнения от числа итераций
В этом случае операции такого рода подходят браузерам только при очень небольшом числе n
. Наиболее эффективный способ ускорения работы программы — это сделать число n
как можно меньше. Ajax позволяет вам доставить все необходимые данные прямо здесь и сейчас.