|
Высокопроизводительные AJAX-приложения
Перевод презентации "High Performance Ajax Applications", подготовленной ведущим специалистом из Yahoo (а теперь уже из Apple) Julien Lecomte. В ней автор освещает некоторые аспекты оптимизации как JavaScript-приложений, так и веб-сайтов вообще. В целом, советов много, и почти все, действительно, по делу. Однако, встречается и откровенная реклама Yahoo :) Мои комментарии далее курсивом.
Часть 1. Разработка для высокой производительностиПланируем и проектируем для высокой производительности - Ориентируемся на производительность с самого первого дня
- Тесно работаем с дизайнерами и менеджерами продукта
- Понимаем рациональность дизайна
- Объясняем компромиссы между дизайном и производительностью
- Предлагаем альтернативы и показываем, что еще возможно (на уровне прототипа)
- Пробуем силы в реализации нетривиального дизайна (нельзя сразу говорит «нет»)
- Помогаем упростить дизайн и взаимодействие с пользователем (добиваемся компромисса)
Разрабатываем высокопроизводительные системы: несколько базовых правил - Лучше меньше — да лучше
- Не делайте ничего ненужного.
- Не делайте ничего, пока это не станет по-настоящему необходимым.
- Нарушайте правила
- Добивайтесь компромиссов и нарушайте сложившиеся методики (best practices), но только в качестве последнего средства!
- Работайте над улучшением ощущаемой производительности
- Пользователи могут немного подождать, если:
- их уведомили соответствующим образом о том, что операция задерживается.
- Пользовательский интерфейс постоянно реагирует на действия пользователя.
- Можно схитрить, если все операции проделать уже после обновления интерфейса пользователя.
Измеряемая производительность - Тестируйте производительность, используя окружение, аналогичное пользовательскому
- Отлаживайте ваш код в процессе разработки
- Автоматизируйте функциональное тестирование и проверку производительности
- Сохраняйте историю изменений, как быстро функционируют различные возможности
- Может быть, стоит оставить некоторое (небольшое) количество отладочного кода на «боевом» сервере
Часть 2. Высокопроизводительная загрузка страницыСпособы ускорения загрузки Вашего сайта от Yahoo!Веб-страница работает в 3 иногда перекрывающихся состояниях: - загрузка
- отрисовка
- исполнение
Следующие правила покрывают, в основном, первое состояние. - Уменьшите количество HTTP-запросов
- Используйте CDN
- Используйте HTTP-заголовок Expires
- Сжимайте компоненты страницы
- Помещайте CSS в начале страницы
- Помещайте скрипты в конец
- Избегайте CSS-выражений (expressions)
- Выносите javascript и CSS во внешние файлы
- Уменьшайте количество DNS-запросов
- Минимизируйте Javascript
- Избегайте редиректов
- Уберите повторяющиеся скрипты
- Настройте ETag'и
- Делайте AJAX кэшируемым
Для более подробной информации можно ознакомиться с переводом этой статьи. Оптимизация активов - Уменьшайте 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. - Также можно обрабатывать все данные на стороне сервера.
- Больше информации доступно в этой статье
- Демонстрационная версия
Что делать с 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. Инструменты для производительности
Информация о статье
Написана: Sat, 22 Dec 2007 05:07:00 EET Авторы: Julien Lecomte
Добавил(а): AlexParamonov at Sun, 07 Dec 2008 19:47:48 EET Изменил(а): AlexParamonov at Sun, 07 Dec 2008 20:19:05 EET
Информация о переводе
Переведено: Fri, 15 Feb 2008 19:48:00 EET Язык оригинала: English Перевод: Николай Мациевский a.k.a sunnybear
Добавил(а): AlexParamonov at Sun, 07 Dec 2008 19:54:31 EET Изменил(а): AlexParamonov at Sun, 07 Dec 2008 20:15:51 EET
|
|