Высокопроизводительные AJAX-приложения
Примечание: ниже перевод презентации "High Performance Ajax Applications", подготовленной ведущим специалистом из Yahoo (а теперь уже из Apple) Julien Lecomte. В ней автор освещает некоторые аспекты оптимизации как JavaScript-приложений, так и веб-сайтов вообще. В целом, советов много, и почти все, действительно, по делу. Однако, встречается и откровенная реклама Yahoo :) Мои комментарии далее курсивом.
Часть 1. Разработка для высокой производительности
Планируем и проектируем для высокой производительности
- Ориентируемся на производительность с самого первого дня
- Тесно работаем с дизайнерами и менеджерами продукта
- Понимаем рациональность дизайна
- Объясняем компромиссы между дизайном и производительностью
- Предлагаем альтернативы и показываем, что еще возможно (на уровне прототипа)
- Пробуем силы в реализации нетривиального дизайна (нельзя сразу говорит «нет»)
- Помогаем упростить дизайн и взаимодействие с пользователем (добиваемся компромисса)
Разрабатываем высокопроизводительные системы: несколько базовых правил
- Лучше меньше — да лучше
- Не делайте ничего ненужного.
- Не делайте ничего, пока это не станет по-настоящему необходимым.
- Нарушайте правила
- Добивайтесь компромиссов и нарушайте сложившиеся методики (best practices), но только в качестве последнего средства!
- Работайте над улучшением ощущаемой производительности
- Пользователи могут немного подождать, если:
- их уведомили соответствующим образом о том, что операция задерживается.
- Пользовательский интерфейс постоянно реагирует на действия пользователя.
- Можно схитрить, если все операции проделать уже после обновления интерфейса пользователя.
Измеряемая производительность
- Тестируйте производительность, используя окружение, аналогичное пользовательскому
- Отлаживайте ваш код в процессе разработки
- Автоматизируйте функциональное тестирование и проверку производительности
- Сохраняйте историю изменений, как быстро функционируют различные возможности
- Может быть, стоит оставить некоторое (небольшое) количество отладочного кода на «боевом» сервере
Часть 2. Высокопроизводительная загрузка страницы
Веб-страница работает в 3 иногда перекрывающихся состояниях:
- загрузка
- отрисовка
- исполнение
Следующие правила покрывают, в основном, первое состояние.
- Уменьшите количество HTTP-запросов
- Используйте CDN
- Используйте HTTP-заголовок Expires
- Сжимайте компоненты страницы
- Помещайте CSS в начале страницы
- Помещайте скрипты в конец
- Избегайте CSS-выражений (expressions)
- Выносите javascript и CSS во внешние файлы
- Уменьшайте количество DNS-запросов
- Минимизируйте Javascript
- Избегайте редиректов
- Уберите повторяющиеся скрипты
- Настройте ETag'и
- Делайте AJAX кэшируемым
Для более подробной информации можно ознакомиться с переводом этой статьи на webo.in.
Оптимизация активов
- Уменьшайте CSS- и JavaScript-файлы:
- Объединяйте CSS- и JavaScript-файлы:
- Оптимизируйте графические ресурсы:
Уменьшайте размер кода, который не минимизируется
- Загрузка и анализ HTML, CSS и JavaScript-кода ресурсоемки.
- Будьте кратки и пишите меньше кода.
- Используйте JavaScript-библиотеки по назначению.
- Может быть, стоит разделить ваши большие JavaScript-файлы на несколько более маленьких (пакетов), если анализ и исполнение скриптов занимает слишком много времени (ошибка Firefox #313967)
- Загружайте код (HTML, CSS и JavaScript) по требованию (a.k.a «ленивая загрузка» или ненавязчивый JavaScript)
Оптимизируем начальную загрузку (1/4). Общие советы...
- Лучше создавать первоначальный вид страницы прямо на сервере:
- Избегайте добавления к странице лишнего объема
- Вам по-прежнему придется прикреплять обработчики событий, как только будет готово DOM-дерево
- Закрывайте HTML-теги для ускорения анализа страницы:
- Может быть, стоит сбрасывать буфер вывода для Apache как можно быстрее:
- Загрузка внешних CSS-файлов (должны быть в самом верху страницы!) может начинаться сразу после объявления тега
<head>
. - Однако, это может не повлиять на скорость отображения браузерами, ибо они, скорее всего, буферизируют данные перед их отображением.
- Загружайте только необходимые ресурсы / загружайте ресурсы с задержкой или по запросу
- Используйте YUI Image Loader
Оптимизируем начальную загрузку (2/4). Не всегда стоит ждать onload
...
- Большинство DOM-операций может быть выполнено перед тем, как сработает событие
onload
. - Если вы обладаете полным контролем нам тем, где можно разместить вызовы скриптов, стоит инициализировать весь ваш код в теге <script>, расположенном прямо перед закрывающим тегом
</body>
. - Также можно использовать метод
onDOMReady
в утилите YUI Event:YAHOO.util.Event.onDOMReady(function () {
// Выполняем какие-нибудь действия...
// например, прикрепляем обработчики событий.
});
Оптимизируем начальную загрузку (3/4). Загрузка скриптов напоследок
- Если ваш сайт хорошо спроектирован, то должен полностью работать и без включенного JavaScript.
- Следовательно, вы можете загружать все скрипты с некоторой задержкой.
- Следуя этому принципу, можно загрузить все остальные ресурсы (файлы стилей, изображения и т.д.) в первую очередь
- Это (визуально) увеличит скорость загрузки сайта
- Прямо перед закрывающим тегом
</body>
добавьте следующее:<script>
window.onload = function () {
var script = document.createElement("script");
script.src = ...;
document.body.appendChild(script);
};
</script>
Оптимизируем начальную загрузку (4/4). Условная предзагрузка
Часть 3. Высокопроизводительный JavaScript
Уменьшаем число запросов для разрешения ссылок: цепочка областей видимости (1/2)
- Разрешение (look-up) ссылки выполнятся каждый раз, когда запрашивается переменная.
- Переменные разрешаются в обратном порядке: от более частной области видимости к более общей.
var g = 7;
function f(a) {
var v = 8;
x = v + a + g;
}
f(6);
Уменьшаем число запросов для разрешения ссылок: цепочка областей видимости (2/2)
- Поэтому старайтесь использовать переменные максимально близко к области их объявления (с помощью ключевого слова
var
) и избегайте использования глобальных переменных любой ценой. - Никогда не используйте ключевое слово
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;
})();
Уменьшаем число запросов для разрешения ссылок: цепочка прототипов
- Доступ к членам объекта, определенным в нем самом, примерно на 25% быстрее, чем доступ к любому члену в цепочке прототипов.
- Чем больше цепочка прототипов (и путь до вызываемого свойства или метода), тем медленнее работает скрипт.
function A () {}
A.prototype.prop1 = ...;
function B () {
this.prop2 = ...;
} B.prototype = new A();
var b = new B();
Оптимизируем вызовы объектов
- Если вам требуется создать много объектов, стоит рассмотреть добавление их к прототипу родительского объекта вместо создания индивидуальных свойств в текущем объекте (для прототипа свойства будут созданы только один раз, а затем этот прототип будет использоваться для создания новых объектов).
- Это также уменьшит объем выделяемой памяти.
- Однако, это замедлит обращение к членам объекта (ведь придется просматривать цепочку прототипов).
// быстрее для создания большого количества одинаковых объектов
function Foo () {...}
Foo.prototype.bar = function () {...};
// быстрее для обращения к свойствам объектов
function Foo () {
this.bar = function () {...};
}
Не используейте eval
!
- Строка, которая передается
eval
(и всем родственным ему методам: конструктору Function
, функциям setTimeout
и setInterval
), должна быть скомилирована и выполнена. Это очень медленно! - Никогда не передавайте строку в вызовы функций
setTimeout
и setInterval
. Вместо это используйте анонимную функция, например:setTimeout(function () {
// код, который нужно выполнить с задержкой
}, 50);
- Никогда не используйте
eval
и конструктор Function
(кроме, может быть, некоторых очень редких случаев, и только в тех блоках, где производительность не является критичной, т.е. расширяемость или логичность модели, например, будут важнее, чем производительность).
Оптимизируем объединение строк
- В Internet Explorer (JScript) объединение двух строк порождает создание новой строки, в которую обе они копируются:
var s = "xxx" + "yyy";
s += "zzz";
- Следовательно, для Internet Explorer будет значительно быстрее добавлять строки в массив, а затем, используя,
Array.join
, объединить (не используйте это для простых объединений, работает сильно медленее!)// медленнее
var i, s = "";
for (i = 0; i < 10000; i++) {
s += "x";
}
// быстрее
var i, s = [];
for (i = 0; i < 10000; i++) {
s[i] = "x";
}
s = s.join("");
- Другие JavaScript-движки (WebKit, SpiderMonkey) уже оптимизированы, чтобы использовать
realloc
+ memcpy
во всех возможных случаях объединения строк. - Используйте YUI Compressor!
Оптимизируйте регулярные выражения
- Не используйте конструктор
RegExp
, если вы не собираетесь создавать регулярное выражение «на лету». Вместо этого используйте постоянные регулярные выражения (regular expression literals). - Используйте метод
test
, если вам нужно просто проверить, соответствует ли строка шаблону (метод exec
немного более ресурсоемкий)if (/loaded|complete/.test(document.readyState)) {...}
- Используйте нефиксирующие (non-capturing) группы (
?:...
) - Придерживайтесь простых шаблонов. Стоить пересмотреть ваши регулярные выражения, если они выглядит примерно так...
(?:(?:\r\n)?[\t])*(?:(?:(?:[^()<>@,;:\\".\[\]\000-\031]+
(?:(?:(?:\r\n)?[\t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:
[^\"\r\\]|\\.|(?:(?:\r\n)?[\t]))*"(?:(?:\r\n)?[\t])*)(?:
\.(?:(?:\r\n)?[\t])*(?:[^()<>@,;:\\".\[\]\000-\031]+(?:(
?:(?:\r\n)?[\t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"
\r\\]|\\.|(?:(?:\r\n)?[\t]))*"(?:(?:\r\n)?[\t])*))*@(?:(
?:\r\n)?[\t])*(?:[^()<>@,;:\\".\[\]\000-\031]+(?:(?:(?:\
r\n)?[\t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]
|\\.)*\](?:(?:\r\n)?[\t])*)(?:\.(?:(?:\r\n)?[\t])*(?:[^(
)<>@,;:\\".\[\]\000-\031]+(?:(?:(?:\r\n)?[\t])+|\Z|(?=[\
["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?
[\t])*))*|(?:[^()<>@,;:\\".\[\]\000-\031]+(?:(?:(?:\r\n)
?[\t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|
(?:(?:\r\n)?[\t]))*"(?:(?:\r\n)?[\t])*)*\<(?:(?:\r\n)?[\
t])*(?:@(?:[^()<>@,;:\\".\[\]\000-\031]+(?:(?:(?:\r\n)?[
\t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*
\](?:(?:\r\n)?[\t])*)(?:\.(?:(?:\r\n)?[\t])*(?:[^()<>@,;
:\\".\[\]\000-\031]+(?:(?:(?:\r\n)?[\t])+|\Z|(?=[\["()<>
@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[\t])*
))*(?:,@(?:(?:\r\n)?[\t]))
Кеширование
Что делать с JavaScript-процессами, которые долго выполняются (1/2)
- В время работы процесса, который долго исполняется, весь UI браузера «замораживается»
- Следовательно, для обеспечения более-менее комфортного восприятия со стороны пользователя, нужно убедиться, чтобы каждый JavaScript-поток не исполнялся более ~ 300 мс (самое большое).
- Можно разбить длительные процесс на ряд более маленьких порций работы и объединить их в цепочку, используя
setTimeout
. - Также можно обрабатывать все данные на стороне сервера.
- Больше информации доступно по адресу http://www.julienlecomte.net/blog/2007/10/28 (перевод)
- Демонстрационная версия
Что делать с JavaScript-процессами, которые долго выполняются (2/2)
function doSomething (callbackFn) {
// Здесь инициализируем данные...
(function () {
// Делаем некоторую работу...
if (termination condition) {
// мы закончили
callbackFn();
} else {
// обрабатываем следующую порцию
setTimeout(arguments.callee, 0);
}
})();
}
Разные советы (1/2)
- Элементарные операции часто быстрее, чем вызовы соответствующих функций:
var a = 1, b = 2, c;
// медленнее
c = Math.min(a, b);
// быстрее
c = a < b ? a : b;
// медленнее
myArray.push(value);
// быстрее
myArray[myArray.length] = value;
// еще быстрее
myArray[idx++] = value;
- По возможности избегайте использования
try...catch
в тех частях, в которых критична производительность:// медленнее
var i;
for (i = 0; i < 100000; i++) {
try {
...
} catch (e) {
...
}
}
// быстрее
var i;
try {
for (i = 0; i < 100000; i++) {
...
}
} catch (e) {
...
}
Разные советы (2/2)
- По возможности избегайте использования
for...in
в тех частях, в которых критична производительность:// медленнее
var key, value;
for (key in myArray) {
value = myArray[key];
...
}
// быстрее
var i, value, length = myArray.length;
for (i = 0; i < length; i++) {
value = myArray[i];
...
}
- Делайте ветвление, по возможности, на самом высоком уровне (относительно условия ветвления):
// медленнее
function fn () {
if (...) {
...
} else {
...
}
}
// быстрее
var fn;
if (...) {
fn = function () {...};
} else {
fn = function () {...};
}
Часть 4. Высокопроизводительный динамический HTML
Изменение дерева документа при помощи innerHTML
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);
}
(сильно быстрее во всех браузерах класса А)
var i, j, el, idx, html;
idx = 0;
html = [];
html[idx++] = "<table>";
for (i = 0; i < 1000; i++) {
html[idx++] = "<tr>";
for (j = 0; j < 5; j++) {
html[idx++] = "<td></td>";
}
html[idx++] = "</tr>";
}
html[idx++] = "</table>";
el = document.createElement("div");
document.body.appendChild(el);
el.innerHTML = html.join("");
Замечание: прочитайте http://www.julienlecomte.net/blog/2007/12/38/ (перевод)
Изменение дерева документа при помощи cloneNode
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);
}
(быстрее по всех браузерах класса А, иногда значительно быстрее)
var i, el, table, tbody, template, row, cell;
el = document.createElement("div");
document.body.appendChild(el);
table = document.createElement("table");
el.appendChild(table);
tbody = document.createElement("tbody");
table.appendChild(tbody);
template = document.createElement("tr");
for (i = 0; i < 5; i++) {
cell = document.createElement("td");
template.appendChild(cell);
}
for (i = 0; i < 1000; i++) {
row = template.cloneNode(true);
tbody.appendChild(row);
}
Замечание: расширенные свойства (expando properties) и прикрепленные обработчики событий будут утеряны!
Изменение дерева документа при помощи DocumentFragment
DocumentFragment
(DOM Level 1 Core) является облегченным вариантом Document
.- Он поддерживает только ограниченное число обычных DOM-методов и свойств.
- Реализация в IE интерфейса для
DocumentFragment
не соответствует спецификации W3C и возвращает обычный объект Document
.
var i, j, el, table, tbody, row, cell, docFragment;
docFragment = document.createDocumentFragment();
el = document.createElement("div");
docFragment.appendChild(el);
table = document.createElement("table");
el.appendChild(table);
tbody = document.createElement("tbody");
table.appendChild(tbody);
for (i = 0; i < 1000; i++) {
...
}
document.body.appendChild(docFragment);
Уменьшайте число обработчиков событий (1/2)
- Прикрепление обработчика событий к сотням элементов весь ресурсоемко
- Наращивание количества обработчиков событий чревато потенциальными утечками памяти
- Решение: использовать
event delegation
, технику, базирующуюся на event bubbling
<div id="container">
<ul>
<li id="li-1">List Item 1</li>
<li id="li-2">List Item 2</li>
<li id="li-3">List Item 3</li>
<li id="li-4">List Item 4</li>
<li id="li-5">List Item 5</li>
...
</ul>
</div>
Уменьшайте число обработчиков событий (2/2)
YAHOO.util.Event.addListener("container", "click", function (e) {
var el = YAHOO.util.Event.getTarget(e);
while (el.id !== "container") {
if (el.nodeName.toUpperCase() === "LI") {
// Что-нибудь делаем...
break;
} else {
el = el.parentNode;
}
}
});
Уменьшайте отрисовки (reflows)
- Отрисовка экрана происходит каждый раз при манипуляциях с DOM-деревом.
- У браузеров есть ряд оптимизаций, чтобы уменьшить число отрисовок, в частности:
- Изменение невидимого элемента (
display:none
) не вызывают отрисовку - Изменение элемента, не входящего в DOM ("off-DOM") не вызывает отрисовку
- Групповое изменение стилей:
- Изменяйте значение атрибута
style
при помощи метода setAttribute
(не работает в Internet Explorer). Пример:el.setAttribute("style", "display:block;width:auto;height:100px;...");
- Изменяйте значение свойства
cssText
объекта style
. Пример:el.style.cssText = "display:block;width:auto;height:100px;...";
- Более масштабируемо: изменяйте название CSS класса у элемента. Пример:
YAHOO.util.Dom.replaceClass(el, "foo", "bar");
Разные советы...
- Может быть, стоит использовать событие
onmousedown
вместо onclick
- Используйте как преимущество удаление небольшой задержки между нажатием кнопки мыши и ее освобождением пользователем.
- «Переключите ваш код на первую передачу»: устраните частые и ресурсоемкие действия
Часть 5. Высокопроизводительный динамический макет и CSS
Разные советы...
- Используйте технику CSS Sprites для быстрого переключения картинок (Snappy Image Replacement, известен также как rollover-эффект).
- Избегайте использования JavaScript для (анимации) макета.
- Нужно помнить о
window.onresize
... - Вместо этого используйте чистый CSS!
- Побочные эффекты: улучшается масштабируемость, улучшается поддержка пользователей с отключенными возможностями (скриптами, изображениями, стилями), и т.д.
- Избегайте использования CSS-выражений (expressions) в Internet Explorer.
- Выражения (в большинстве своем) постоянно пересчитываются, чтобы учесть текущие изменения на странице.
- Существуют способы по их оптимизации, но, в общем случае, можно найти обходные пути, чтобы их не использовать.
- Избегайте использования CSS-фильтров в Internet Explorer (или сведите их к минимуму).
- Оптимизируйте табличную разметку.
- Задача: позволить движку браузера начать отображение таблицы перед тем, как он ее полностью получит.
- Используйте
table-layout:fixed
- Дополнительно определите элемент
COL
для каждой колонки. - Задайте значение для его атрибута
WIDTH
.
- Оптимизируйте ваши CSS-селекторы [http://developer.mozilla.org/en/docs/Writing_Efficient_CSS]. Стоит заметить, что большая часть этих советов, в связи с проведенным исследованием, не несет особого смысла.
Часть 6. Высокопроизводительный Ajax
Практический Ajax
- Никогда не применяйте синхронный
XMLHttpRequest
. - Программно обрабатывайте сетевые тайм-ауты.
- Решение: используйте YUI Connection Manager:
var callback = {
success: function () { /* Что-нибудь делаем */ },
failure: function () { /* Что-нибудь делаем */ },
timeout: 5000
};
YAHOO.util.Connect.asyncRequest("GET", url, callback);
Улучшайте видимые сетевые задержки используя оптимистичный шаблон
- Если данные проверяются локально (на клиенте при помощи JavaScript) перед их отправкой на сервер, то запрос будет успешным в 99,9% случаев, (также фактическая реакция «сервера» на действия пользователя будет более быстрой).
- Следовательно, для оптимизации пользовательского восприятия стоит предполагать успешный результат и прибегать к следующему шаблону:
- Обновить UI при отправке запроса.
- Заблокировать UI/структуру данных, по возможности, наиболее точечно.
- Дай пользователю понять, что что-то произошло.
- Дать пользователю понять, что объект UI заблокирован.
- Разблокировать UI/структуру данных, если результат был успешным.
- Максимально мягко обрабатывать возможные ошибки.
Разные советы...
- Помните о максимальном количестве одновременных HTTP/1.1 соединений.
- По возможности множьте ваши Ajax-запросы, если ваш сервер это сможет поддерживать.
- Добавляйте дополнительные сообщения в ответ для Ajax-запроса.
- Выберите JSON вместо XML в качестве формата обмена данных
- Доступ к JSON-данным осуществляется проще и требует меньше ресурсов, чем для XML.
- У JSON меньше накладных издержек.
- Добавляйте, не спрашивая. Используйте технологию COMET для отправки уведомлений браузеру в режиме реального времени.
- Рассмотрите возможность использовать локальные данные (local storage) или локальный кеш, запрашивая с сервера только изменения:
userData
для Internet Explorer- Локальные данные для Flash
DOM:Storage
(стабильное API для хранения данных от WhatWG, реализовано в Firefox 2)- Google Gears
- и т.д.
Часть 7. Инструменты для производительности
Читать дальше
Все комментарии (habrahabr.ru)