Преждевременная оптимизация JavaScript

Одна из наиболее цитируемых максим в программировании сформулирована Дональдом Кнутом в его знаменитой статье "Structured Programming With Go To Statements":

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

Важен контекст, обрамляющий данную цитату. Утверждение состоит в том, что оптимизация 97 % некритичных ветвей кода ведет к созданию менее понятного/более трудного в обслуживании кода при незначительных или отсутствующих преимуществах. Но это подразумевает, что оптимизация 3 % критически важных ветвей кода весьма важна. Таким образом, эта максима обозначает следующее: "Оптимизация некритичных ветвей кода — корень всех зол".

Это наполненное смыслом значение, которое вложил в данную фразу Кнут, практически полностью теряется, когда разработчики используют фразу "Не оптимизируйте преждевременно", чтобы показать несостоятельность кода, который они считают чрезмерно сложным при отсутствии возможных преимуществ. Конечно, такая критика является очень субъективной — то, что чрезмерно сложно для тебя, может оказаться полностью понятным и удобным для меня. Кроме того, то, что эффективный код всегда более сложен, не соответствует действительности, в то время как обратное чаще всего справедливо.

Ключ к соблюдению заповеди Кнута заключается, конечно же, в том, как определить, что является "критически важной ветвью кода", а что — "некритичной". Является ли утверждение о разделении 97 к 3 фактом или только предположением? С первого взгляда, ответ кажется простым и очевидным, но если копнуть глубже, оказывается, что он очень сложный. Например, то, что может быть критически важной ветвью для одной ситуации применения программного обеспечения, может оказаться некритичной ветвью в другой ситуации и наоборот. Должен ли код в "критически важной ветви" соответствовать тем же требованиям к стилю кода, которые предъявляются и к другому коду в проекте?

Чтобы извлечь что-то полезное из всех этих сложностей, многие люди предлагают более прагматичное определение преждевременной оптимизации: оптимизация любого участка кода до того, как вы заметите, что он выполняется недопустимо медленно или неэффективно. Другими словами, код следует рассматривать в качестве претендента на оптимизацию "критически важной ветви" только в том случае, если вы имеете возможность наблюдать, как он замедляет работу всего программного обеспечения. В противном случае он должен считаться некритичным и не должен оптимизироваться.

Но как же мы определим термин наблюдаемый? Принято думать, что люди не могут воспринимать время реагирования менее 100 мс в причинно-следственных связях. Поэтому если некоторая часть кода не замедляет работу на 100 мс или более, то этот факт не является наблюдаемым.

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

Скорость или смерть

Преждевременная оптимизация часто сравнивается со "смертью от тысячи порезов"; она считается бесполезной, потому что направлена на множество мелких вещей, которые сам по себе не являются критически важными, при пессимистичном предположении, что совместно все они вызовут снижение производительности компьютера (наблюдаемым образом). Практика показывает, что одновременно проявляются только несколько из этих "порезов", что не заслуживает особого внимания.

Хотя такая позиция разумна и уравновешена, другая проблема заключается в том, сколько вреда даже несколько таких "порезов" могут принести, если им не уделяется должного внимания. Другими словами, более эффективным по сравнению с субъективным выбором конкретных оптимизаций обычно является уделение повышенного внимания тому, как и где эти "порезы" проявляют себя, чтобы можно было эффективнее избегать их.

А сейчас давайте отложим обсуждение того, чем является или не является преждевременная оптимизация, и рассмотрим некоторые способы увеличения скорости выполнения JavaScript. Будем надеяться, что это обсуждение поможет вам пересмотреть и улучшить методы написания кода и позволит написанному вами коду быть "быстрым по умолчанию" (см. материалы конференции Velocity).

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

"Незрелая"оптимизация — корень всех зол.

Обычные подозреваемые

Где нам следует начать поиск областей кода, которые необходимо оптимизировать? Существуют ли шаблоны, которые с наибольшей вероятностью создают наблюдаемые "узкие места" в производительности? Как оптимизировать код, не сделав его менее понятным и более трудным в обслуживании?

Мы рассмотрим 3 основные области написания кода JavaScript, в которых наиболее часто наблюдается снижение производительности программ из-за недостаточной (или даже незрелой) оптимизации кода: использование циклов, доступ к переменным/свойствам и обход/использование модели DOM.

Ни в коем случае не следует считать указанные аспекты единственными проблемными областями, но с большой долей вероятности именно они в первую очередь будут причиной низкой производительности вашего приложения. Если вы сможете уделять больше внимания данным аспектам, производительность вашего приложения будет значительно более оптимальной по умолчанию.

Использование циклов

Вас не должно удивлять, что использование циклов может вызывать проблемы с производительностью приложения. Если вы выполняете небольшую, но неэффективную операцию, и повторяете ее десятки, сотни или тысячи раз, это может оказывать весьма значительное влияние. Если вы используйте метод, на обработку которого подсистема JavaScript затрачивает на половину миллисекунды больше, и помещаете его в цикл с 1000 повторений, вы создаете заметную задержку в 500 миллисекунд.

Давайте рассмотрим несколько распространенных ситуаций, в которых разработчики непреднамеренно замедляют работу в цикле. Ознакомьтесь со следующим кодом:

function countOdds(arr) {
      var num = 0;
      for (var i=0; i<arr.length; i++) {
          if (arr[i] % 2 == 1) num++;
      }
      return num;
  }
   
  var nums = [], num_odds;
 
// создание списка из 1000 случайных чисел
for (var j=0; j<1000; j++) {
    nums[nums.length] = Math.round(Math.random()*10);
    num_odds = countOdds(nums);
}
 
console.log(num_odds);

Обратите внимание, что в строке 14 "countOdds(...)" вызывается внутри цикла, то есть каждую итерацию (1000 раз). Но кроме такого вызова, внутри "countOdds" имеется еще один цикл. Таким образом, внутренний цикл выполняется при каждом вызове, то есть при каждой итерации по внешнему циклу.

Это может показаться глупым примером, однако такая ситуация достаточно распространена в случаях, когда в процессе рефакторинга кода что-то, например внутренний цикл, перемещается в другую функцию, что скрывает причину снижения производительности при вызове такой функции. Если бы вы не могли видеть реализацию "countOdds", вы бы, возможно, не узнали о выполнении внутреннего цикла.

Конечно, в этом натянутом примере отсутствует причина для постоянного пересчета "countOdds". Для повышения производительности этот расчет может быть выполнен в самом конце. Но если бы была причина, по которой пересчет числа нечетных чисел требовался бы на каждой итерации, то вы бы просто учли такое негативное влияние на производительность.

Вы бы могли применить другой алгоритм для решения данной проблемы иным способом, чтобы избежать снижения производительности:

var nums = [], num_odds;
  
 // создание списка из 1000 случайных чисел
 for (var j=0; j<1000; j++) {
     nums[nums.length] = Math.round(Math.random()*10);
     if (nums[nums.length-1] % 2 == 1) num_odds++;
 }
  
 Console.log(num_odds);

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

Если вы перемещаете код в функцию, так как он будет вызываться из разных расположений, возможно, что такой рефакторинг имеет смысл с точки зрения удобства обслуживания кода. Но если вы просто переносите отдельный блок линейного кода в отдельный вызов функции, вы лишь немного улучшаете удобочитаемость кода и закладываете основу для потенциального снижения производительности.

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

Ниже приведен другой пример вызова функции внутри циклов:

function myData() {
     var idx = 0, data = [];
     
     this.next = function() {      return data[idx++]; };
     this.add = function(val) { data[data.length] = val; };
      this.size = function() { return data.length; };
 }
  
 var obj = new myData();
 
for (var i=0; i<1000; i++) {
    obj.add(Math.round(Math.random()*10));
}
// ...
for (var j=0; j<obj.size(); j++) {
    console.log(obj.next());
}

Здесь мы видим очень распространенный шаблон в объектно-ориентированном программировании: идея создания "структуры данных", которая инкапсулирует некоторые данные и предоставляет некоторые общие методы для работы с этим набором данных (или его итерации). Хотя это определенно имеет свои преимущества с точки зрения стиля/структуры кода, нам следует помнить о потенциальном влиянии на производительность.

Например, в строке 12 мы видим, что структура данных не предоставляет никакого API для "массовой загрузки" данных, поэтому необходимо вызывать функцию "add" на каждой итерации первого цикла "for". Что если API можно было бы немного видоизменить для разрешения получения функцией "add" массива, который мы могли бы создать в цикле "for" и выполнить один вызов функции/метода для структуры данных, чтобы передать его? В данном примере это сэкономило бы 999 вызовов функции, а у нас бы все равно была довольно хорошая "объектно-ориентированная" абстракция структуры данных.

Кроме того, во втором цикле мы видим два вызова метода, выполняемых для структуры данных на каждой итерации. Первый из них (строка 16) очевиден: "obj.next()". Второй же (строка 15) заметить в коде труднее. "obj.size()" вызывается в качестве метода доступа для внутреннего свойства. Но что если эта структура данных просто имеет автоматически обновляемое общее свойство "size", которое мы можем проверить вместо вызова функции/метода?

И что если вместо вынужденного многократного вызова "obj.next()" мы бы могли запросить массовый дамп элементов, передав числовой параметр в "next"? Вместе эти два изменения в рамках данного примера сэкономят 1998 вызовов метода, а наша структура данных практически не потеряет своего "объектно-ориентированного" стиля. Взгляните:

function myData() {
     var idx = 0, data = [];
     
     this.size = 0;
     this.next = function(num) {
         if (num) {
             return data.slice(idx,idx+=num);
         }
         return data[idx++]; 
    };
    this.add = function(val) {
        if (Object.prototype.toString.call(val) == "[object Array]") {
            data = data.concat(val);
            this.size = data.length;
        }
        else {
            data[data.length] = val;
            this.size++;
        }
    };
}
var obj = new myData();
 
for (var i=0, tmp=[]; i<1000; i++) {
    tmp[tmp.length] = Math.round(Math.random()*10);
}
obj.add(tmp);
 
//...
 
tmp=obj.next(obj.size);
for (var i=0; i<tmp.length; i++) {
    console.log(tmp[i]);
}

Заметка на полях: объектно-ориентированное программирование порождает соблазн смоделировать все в коде объектно-ориентированным образом. По понятным причинам мы любим красоту инкапсулированные структуры данных, итераторы и т. п. Но в некоторых случаях, особенно при работе с динамическими языками, такими как JavaScript, мы можем принести такую малую жертву в виде шаблона/стиля кода ради перспективы значительного увеличения производительности. Гибкость (например, "массовые" операции для предотвращения многократных вызовов функции)иногда прощезаложить в самом начале, чем позднее осуществлять рефакторинг в API объекта (и всех случаях его использования) при обнаружении узких мест производительности.

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

jQuery (использование циклов)

Я специально хочу рассмотреть распространенную ситуацию снижения производительности, которая часто возникает в безобидном коде поиска, таком как циклы jQuery с "$.fn.each" и тому подобные. Повторюсь, что циклы просто стремятся превратить небольшие погрешности производительности в значительные проблемы.

Изучите следующее:

$("a").each(function(){
     var $a = $(this);
     if ($a.attr("rel") == "foo") {
         $a.addClass("foo");
     }
      else {
         $a.addClass("other");
     }
 });

В данном фрагменте кода мы требуем от jQuery включить в цикл каждый элемент ссылки <a> на странице, и мы снабжаем каждый из них элементом className. То, какой класс добавляется, зависит от атрибута "rel" элемента. Вас следует спросить себя что случится, если этот код выполняется на странице с 1000 ссылок? Сейчас вы можете ориентироваться на страницу с несколькими ссылками, но не откажется ли этот код работать через 2 месяца, когда на странице будет размещено значительно больше содержимого?

Давайте пока проигнорируем, что этот шаблон jQuery неявно приводит к одному вызову анонимной функции (определение, начинающееся со строки 1) для каждой итерации цикла. Выше мы уже обсуждали, насколько распространены такие шаблоны и какие проблемы с производительностью они могут вызывать в зависимости от числа совершаемых итераций.

Внутри данной функции имеется ссылка на объект ("this"), которая привязана jQuery к конкретному элементу модели DOM <a> для текущей итерации. Довольно часто (на деле — в подавляющем большинстве случаев) нам требуется обработанные jQuery ссылка на этот объект "this", а не необработанная ссылка на элемент DOM.

Как и в данном примере, это вызвано тем, что нам нравятся удобные модули поддержки jQuery "attr(...)" и "addClass(...)", которые доступны при использовании объекта коллекции jQuery. Мы просто не знаем или не заботимся о том, что для получения этих модулей поддержки мы фактически просто используем объект коллекции для создания оболочки вокруг отдельного элемента (строка 2).

Однако именно здесь нас и поджидает самое значительное потенциальное снижение производительности. По причинам, которые выходят за рамки данной статьи, вызов "$(...)" и передача объекта не так просты, как использование массива/коллекции для создания оболочки вокруг объекта.

jQuery имеет полный набор сложных внутренних функциональных возможностей для управления коллекциями/обходом/стеком, и хотя мы просто передаем отдельный объект для получения доступа к функциям модулей поддержки, необходимо учитывать всю структуру целиком. Неудивительно, что на практике "$(...)" не является быстрым/эффективным вызовом функции.

К счастью, существует решение проблемы с медленной работой "$(...)" для случая, когда мы хотим использовать объект коллекции для создания оболочки вокруг всего одного элемента. Этот код был создан под влиянием полезного подключаемого модуля each2 jQuery от Бена Алмана (Ben Alman) (@cowboy):

var $a = $(0);
 $("a").each(function(){
     $a.context = $a[0] = this;  // подделка объекта коллекции
      if ($a.attr("rel") == "foo") {
        $a.addClass("foo");
      }
    else {
         $a.addClass("other");
     }
});

В строке 1 за пределами цикла мы задаем заглушку коллекции, назначение которой можно изменять для каждой итерации цикла. В строке 3 мы подделываем создание оболочки с использованием объекта коллекции посредством прямого присвоения (вместо вызова функции "$(...)"), избегая большинства проблем с производительностью. Как показывает тест производительности, хотя этот тип методики немного хуже выглядит, но он позволяет значительно повысить производительность.

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

Доступ к переменным/свойствам

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

Первым подводным камнем является поиск по области, как показано ниже:

function foo() {
     // ...
     function bar() {
         // ...
         function baz() {
             console.log(fun);
         }
     }
 }
 
var fun = "weee!";
foo();

В данном примере внутренней функции "baz" при вызове (не показано) требуется определить место для поиска ссылки на переменную "fun". Сначала она выполняет поиск в своей области локальной функции ("baz"), затем в области родительской функции ("bar"), затем в области "foo", и, наконец, в глобальной области, где она и обнаруживает "fun". Поиск по области удобен, но он забирает небольшую часть производительности.

В нашем примере "fun" используется лишь один раз, поэтому не создает серьезных проблем. Но представьте, что доступ к "fun" осуществлялся бы тысячи раз внутри "baz" (как в цикле; см. выше); в этом случае такие доли миллисекунд могут накапливаться. Вряд ли это привело бы к потере более чем нескольких десятков миллисекунд даже в самом худшем случае, но, как я уже говорил ранее, чем больше таких аспектов имеет место одновременно, тем вероятнее наличие заметного снижения производительности. Нет ничего сложного в том, чтобы думать о таких вещах во время написания кода, а в некоторых случаях — предпринимать упреждающие действия и устранять данную проблему еще до того, как она проявит себя.

Если в данном случае "baz" требуется часто считывать значение "fun", мы можем объявить локальную копию "fun" внутри "baz", что сокращает цепочку поиска для каждого использования с 4 прыжков до 1:

function foo() {
     // ...
     function bar() {
         // ...
         function baz() {
             var fun = window.fun;
             console.log(fun);
         }
     }
}
 
var fun = "weee!";
foo();

Заметка на полях: будьте осторожны с созданием локальных псевдонимов, если вам требуется изменять значение переменной. Поскольку в данном примере "fun" является строкой примитива, локальное присвоение будет выполняться за счет копии значения, а не за счет ссылки, поэтому изменение "fun" внутри области "baz" не окажет никакого влияния на глобальную копию. Для таких изменений используйте "window.fun", что аналогично присвоению в строке 6.

Также имеется схожая проблема — поиск по цепочке прототипов. Например:

var foo = { fun: "weee!" },
     bar = Object.create(foo),
      baz = Object.create(bar)
  ;
  console.log(baz.fun);

Заметка на полях: я использую новый встроенный в ES5 объект "Object.create()" в качестве небольшого примера создания объектов, соединенных посредством цепочки прототипов. Написать модификатор для браузеров, которые еще не поддерживают "Object.create" из ES5, довольно просто, как показано здесь.

В данном примере "baz" является объектом-потомком (то есть "дочерним элементом") объекта "bar", который в свою очередь является дочерним элементом объекта "foo". В соответствии с кодом "foo" — это объект, содержащий свойство "fun". В отличие от ваших возможных представлений о наследовании прототипов "bar" и "baz" не содержат в себе копию "foo". Вместо этого все объекты динамически связываются через скрытую цепочку прототипов так, что если я запрошу свойство "fun" в объекте "baz" и оно не будет найдено, будет выполнен обход всей цепочки прототипов до нахождения "fun" (или до достижения конца цепочки).

Как и указанный выше обход областей, такой обход цепочки прототипов требует не очень много ресурсов. Если же вам необходимо обратить к свойству через цепочку прототипов несколько раз, вы можете смягчить такое уменьшение производительности, объявив локальную копию или ссылку для переменной. При этом следует соблюдать указанные выше меры предосторожности: используйте данный метод только для считывания.

Еще одним схожим сценарием является доступ к вложенному свойству, который часто имеет место при использовании в коде "пространств имен" для объединения переменных и функций, как показано ниже:

Var foo = {
     bar: {
          baz: {
             fun: "weee!";
         }
     }
  };
  console.log(foo.bar.baz.fun);

Понимаю, что это похоже на заевшую пластинку, но здесь представлен еще один пример такого доступа к "fun", который при многократном повторении оказывает некоторое негативное влияние на производительность из-за поиска по цепочке прототипов. Для разрешения свойства "fun" подсистеме JavaScript приходится искать каждое свойство в цепочке (если она не находит его, проверьте цепочку прототипов) перед разрешением следующего уровня — и так до тех пор, пока она не доберется до "fun".

Не удивительно, что вы собираетесь реализовать многократное считывание вложенного в данное пространство имен свойства "fun", устранить такое небольшое снижение производительности можно, просто создав локальную копию переменной и используя ее. Не забывайте: считывание, а не запись.

Обход и использование модели DOM

Никакое обсуждение узких мест производительности в JavaScript не может быть завершено без рассмотрения, возможно, самой большой проблемы: API модели DOM. Модель DOM сравнивалась с мостом, на котором взимается подорожная пошлина. Каждый раз при пересечении этого моста от JavaScript к модели DOM, например для поиска элемента или изменения его, вы расплачиваетесь производительностью. Во многих случаях это снижение производительности довольно высоко. По сравнению с ним рассмотренные ранее снижения производительности могут показаться незначительными.

Перед рассмотрением кода давайте проведем более подробную аналогию с мостом, на котором взимается подорожная пошлина, так как это касается оптимизации производительности. Представьте, что у вас есть 1000 котят, которых вам нужно перевести мост от своего дома к дому бабушки. При каждом пересечении моста (в обоих направлениях) вы платите пошлину в размере 1 доллара. В вашу машину одновременно может поместиться 50 котят. Но вы можете занять у соседа прицеп, вмещающий в себя еще 200 котят. За использование этого прицепа вы заплатите дополнительную пошлину в размере 0,50 доллара.

Станете ли вы совершать 20 поездок "туда-обратно" (всего 40 долларов) только на машине или потратите немного времени на подготовку к каждой поездке, загрузив 250 котят в машину и прицеп, и совершите всего 4 поездки "туда-обратно" (потратив всего 12 долларов)? Ответ вполне очевиден.

Именно эту концепцию мы и берем на вооружение. Если вы собираетесь использовать модель DOM, лучше это делать посредством большого набора операций, и не посредством набора отдельных вызовов по использованию модели DOM.

Другими словами, вам нужно пересекать описанный выше мост для того, чтобы попасть в магазин и узнать, завезли ли в него ваше любимое печенье. Хотите ли вы пересекать мост каждый день? Или вы хотите посетить магазин один раз и узнать, что печенье появится только на следующей неделе (и не совершать остальные ненужные поездки)?

Я уверен, что все это совершенно очевидно для большинства читателей, и вам может показаться, что я таким образом стараюсь принизить вас. Но вы будете удивлены, насколько легко попасть под воздействие чар магически и удивительно простого синтаксиса такой библиотеки, как jQuery, и совершенно забыть обо всех задачах оптимизации во время написания кода. Я не обещаю, что это позволит вам написать идеально работающий код, но надеюсь, что данное напоминание заставит вас обратить внимание на такие проблемы при следующем открытии редактора кода.

Ознакомьтесь со следующим кодом:

function makeUL(list) {
     $("<ul></ul>").attr({"id":"my_list"}).appendTo("body");
     
     for (var i=0; i<list.length; i++) {
         $("<li></li>").text(list[i]).appendTo("#my_list");
     }
 }

Здесь мы выполняем очень простую задачу: создаем элемент <ul> и добавляем один дочерний элемент <li> в список для каждого элемента массива. Приведенный выше код читается довольно легко. Этап 1. Создаем элемент <ul> и добавляем его на страницу. Этап 2. В цикле просматриваем список и добавляем новый элемент <li> для каждого элемента массива. Все так?

Хотя достаточно ясно, что в приведенном выше коде потенциальное снижение производительности принесено в жертву простоте и удобочитаемости, вас удивит, сколько опытных специалистов по jQuery время от времени пишут код именно так. В некоторых случаях благодаря хорошему знанию правил мы можем намеренно нарушать их, соблюдая необходимую осторожность. Но в других случаях нам просто приходится писать код быстро, и мы просто не видим проблему при использовании данных модульного теста, состоящих из 3 элементов массива.

Но когда впоследствии мы подключаем настоящие данные, выполняется добавление 1000 элементов и наш код практически едва работает. Нам приходится быстро возвращаться к коду и выполнять рефакторинг. Само по себе это хорошо, но рефакторинг всегда является более рискованным (особенно, если не были проведены правильные полные модульные тесты для обеспечения устойчивости кода к регрессии).

Наиболее опытные разработчики JavaScript признают, что снижение производительности из-за некачественного кода модели DOM (такого как предыдущий) достаточно значительно, поэтому обычно усилия, направленные на написание правильного кода с первого раза, являются оправданными. Сравните приведенный выше код со следующим кодом:

function makeUL(list) {
     var $ul = $("<ul></ul>").attr({"id":"my_list"});
     
     for (var i=0; i<list.length; i++) {
         $("<li></li>").text(list[i]).appendTo($ul);
     }
     $ul.appendTo("body");
 }

В данном фрагменте кода мы решили проблемы с производительностью как для считывания, так и для записи. Посмотрите на строку 5 предыдущего фрагмента кода и вы увидите, что каждый раз, когда требовалось добавить элемент <li> в список, мы повторно отправляли запрос для модели DOM по идентификатору "id" ("#my_list"). В данном фрагменте кода мы храним ссылку на элемент <ul> в локальной переменной "$ul". При каждом обращении к элементу или внесении изменений в него мы используем помещенную в кэш ссылку и не тратим ресурсы на выполнение запросов.

Кроме того, мы создали в памяти элемент <ul>, который еще не добавлен в модель DOM. Мы можем свободно изменять данный элемент непосредственно в памяти (по аналогии с фрагментом документа), добавлять в него элементы и т. п., при этом мы не платим "пошлину" для модели DOM, так как рабочая модель DOM еще не знает о элементе <ul> в памяти. После внесения всех необходимых элементов в расположенный в памяти элемент, мы вставляем весь этот элемент (вместе с его дочерними элементами) в рабочую модель DOM с помощью одного вызова "appendTo(...)".

Другими словами, мы загружаем в машину всех котят и совершаем одну поездку через мост, заплатив пошлину один раз. А пока мы находимся на той стороне, стоит проверить, как обстоят дела с печеньем в магазине.

Чтобы увеличить полезность кэширования запроса модели DOM, объедините несколько приведенных советов по повышению производительности; ниже приведен окончательный вариант фрагмента кода:

var $list = $("#my_list"), $a = $(0);
  
 $("a").click(function(){
     $a.context = $a[0] = this;
     var $tmp_ul = $("<ul></ul>"),
         rel = $a.attr("rel")
     ;
     for (var i=0; i<100; i++) {
         $("<li></li>").text(rel + ":" + i).appendTo($tmp_ul);
    }
    $list.children().replaceWith($tmp_ul.children());
    return false; // отмена обработки события по умолчанию <a>
});

Нам нужно получить ссылку на элемент модели DOM "my_list" только один раз, мы так и поступаем и кэшируем ее в переменной "$list". Все функции обработки щелчков мышью могут совместно использовать эту отдельную ссылку, поэтому потребность в повторных запросах элемента отсутствует. Как и раньше, мы создаем временный элемент <ul> с именем "$tmp_ul", в который добавляем 100 элементов списка, а затем заменяем все дочерние элементы рабочего "$list" на дочерние элементы расположенного в памяти элемента "$tmp_list". Наконец, обратите внимание на то, что мы извлекаем атрибут "rel" из выбираемого элемента <a> только один раз, и затем используем эту локальную строковую переменную в цикле вместо выполнения повторных запросов.

Заметка на полях: я уверен, что при взгляде на последний фрагмент кода специалисты по jQuery кричат: "Используй .live() или .delegate()!". И они правы. Если вам необходимо выполнить множество одинаковых операций обработки событий для множества элементов модели DOM, значительно лучше осуществить делегирование одному обработчику, чем 1000. Данный фрагмент кода предназначен для иллюстрации того, как описанные в данной статье способы оптимизации можно объединить в одном блоке кода. Урок: оптимизация производительности кода имеет множество уровней, а оценка кода (иногда многократная) для поиска возможностей для улучшения требует значительного внимания.

$(“#wrap”).up();

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

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

Беспокойтесь не столько о преждевременной, как о незрелой оптимизации. Спрятав голову в песок и думая, что микронастройка производительности JavaScript не стоит времени, вы с такой же вероятностью получите некачественный код, как и в случае слишком рьяной и излишней оптимизации. Когда дело доходит до оптимизации производительности, я бы отдал предпочтение повышенной скорости, а не повышенной осторожности.

Для ускорения работы кода важнее всего не полагаться только на более быстродействующие подсистемы браузера и не игнорировать вашу обязанность (и возможность) как инженера по эффективному определению ситуаций, в которых требуется писать более качественный код с самого начала.

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