Статьи Архив статей

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

JavaScript: Хорошие части. Часть 6: производительность Ajax

Примечание: ниже находится перевод презентации от Douglas Crockford JavaScript: The Good Parts. Part Six: Ajax Performance, в которой освещаются фундаментальные проблемы проектирования клиентских веб-приложений и эффективная их оптимизация. Мои комментарии далее курсивом.

Если в руках только молоток, то любая проблема выглядит как веб-страница.

Революция Ajax

Каждая страница является веб-приложением, которое использует сервер как хранилище данных.

Когда пользователь что-то делает, мы посылаем JSON-запрос на сервер и получаем в ответ JSON-сообщение.

Со стороны сервера JSON-сообщение гораздо легче создать, его быстрее передать по сети, и самому браузеру проще обработать и отобразить такое сообщение, чем HTML-документ.

Рисунок 1. Передача сообщений между браузером и сервером: HTML и JSON

Разделение труда

Как приложение распределяется между браузером и сервером?

Маятник отчаяния

  • Браузер — это терминал.
  • Сервер — это файловая система.

Ищите что-то среднее, например, элементарный диалог между отдельными частями системы.

Ajax'ифицируем

Клиент и сервер находятся в постоянном диалоге. Сообщения между ними должны быть максимально короткими (но не стоит забывать об издержках на передачу сообщений, в частности, о среднем заголовке в 500 байтов).

Клиенту не нужна копия базы данных: в каждый конкретный момент ему лишь нужно достаточно информации для обслуживания пользователя.

Но и не стоит переписывать серверное приложение на JavaScript.

Бедный браузер

Браузер — совсем не эффективная платформа для приложений. Если ваше веб-приложение раздуется, то производительность сильно упадет.

Клиентское программирование должно быть максимально легким.

Удивительно, но браузер работает

Хотя и страшно медленно.

В любом случае, это очень сложная платформа для разработок: существует большое количество проблем, связанных как с безопасностью, так и с производительностью.

Браузер не разрабатывался как платформа для приложений (кроме разве что Google Chrome). Да и Ajax очень сильно нагружает браузер.

Правильность прежде всего

Оптимизация ничего не стоит, если приложение еще не работает правильно: если что-то происходит не так, не важно, насколько быстро это происходит.

Однако тестировать производительность нужно на ранних стадиях разработки. Причем стоит тестировать в условиях, максимально приближенных к боевым: медленная связь, медленные компьютеры. Локальная сеть и мощные машины веб-разработчиков могут скрыть проблемы производительности.

Преждевременная оптимизация является корнем всего зла.

Donald Knuth

Несколько советов для эффективной оптимизации

  • Используйте YSlow для уменьшения времени загрузки веб-приложения.
  • Не оптимизируйте, пока это не нужно. Постарайтесь как можно раньше узнать, что это нужно.
  • Чистый, правильный код легче оптимизировать.
  • Тонкая настройка обычно не дает эффекта.
  • Иногда требуется рефакторинг кода или его перепланирование.

Пример

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 к одному-единственному.

Используйте Ajax-библиотеки по назначению

Эффективное повторное использование уже написанного кода позволит вашим виджетам работать более эффективно.

На что IE8 тратит процессорное время

Рисунок 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 мс (в таком случае хорошо прогонять множественные циклы из элементарных операций и замерять общее время выполнения). Однако стоит помнить и о том, что даже точные вычисления могут привести к неверным выводам.

O (1)

В этом случае операция выполняется только один раз, и особо оптимизировать ее не имеет смысла. Однако если это время инициализации библиотеки, стоит помнить о том, что оно включается во время загрузки каждой страницы сайта.

O (n)

Стоит оптимизировать операции, которые выполняются многократно. В этом случае производительность будет линейно зависеть от числа запусков.

Рисунок 4. Линейная зависимость времени выполнения от числа итераций

Стадии ошибки

Время выполнения операций может проходить несколько стадий:

  • Неэффективность

    Рисунок 5. Неэффективность для времени выполнения

  • Дискомфорт

    Рисунок 6. Дискомфорт для времени выполнения

  • Срыв

    Рисунок 7. Срыв для времени выполнения

O (n log n)

Рисунок 8. Линейно-логарифмическая зависимость времени выполнения от числа итераций

O (n2)

Рисунок 9. Степенная зависимость времени выполнения от числа итераций

В этом случае операции такого рода подходят браузерам только при очень небольшом числе n. Наиболее эффективный способ ускорения работы программы — это сделать число n как можно меньше. Ajax позволяет вам доставить все необходимые данные прямо здесь и сейчас.

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

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