Эффективный JavaScript (часть 1 — ядро языка)

Статья с dev.opera.com, достаточно старая (ноябрь 2006), но для начала вполне подходящая.

Оригинал: http://dev.opera.com/articles/view/efficient-javascript/
Автор: Mark ‘Tarquin’ Wilton-Jones
Перевод: Григорьев Олег
Тип перевода: Весьма вольный
Лицензия: Creative Commons Attribution, Non Commercial — Share Alike 2.5 license

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

Обычное приложение в большинстве случаев собирается компилятором из исходных кодов. Компилятор при этом имеет достаточно времени для того, чтобы как можно лучше оптимизировать его работу. Веб-приложение такой роскоши лишено. Так как оно должно работать во множестве различных браузеров, на различных платформах с разными архитектурами, оно не может быть изначально скомпилировано. Браузеру приходится интерпретировать и компилировать код при каждой загрузке. И всё это должно работать так же гладко и быстро, как и обычное приложение. Причём работать на большом количестве разнообразных устройств, от настольного компьютера до мобильного телефона.

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

В этой статье описано некоторое количество простых приёмов, которые могут улучшить работу ваших веб-приложений. Приёмы эти относятся к трём областям — ядру языка (стандарта ECMAScript), взаимодействию с DOM и загрузке документа.

Ядро языка (ECMAScript)

  1. Избегайте использования eval и конструктора Function
  2. Избегайте использования with
  3. Не используйте try-catch-finally внутри критических функций
  4. Изолируйте eval и with
  5. Избегайте глобальных переменных
  6. Остерегайтесь неявных преобразований объектов
  7. Избегайте for-in в критических функциях
  8. Конкатентация строк
  9. Примитивные операции могут быть быстрее вызовов функций
  10. Используйте функции, а не строки в setTimeout() и setInterval

Избегайте использования eval и конструктора Function

При каждом вызове eval или Function, производится разбор строки с исходным кодом. Движок JS должен при этом интерпретировать исходный код и перевести его в исполняемый. Это достаточно трудоёмкая операция — часто в сотни раз более медленная, чем простой вызов функции.

Функция eval особенно плоха, поскольку содержание интерпретируемой строки не известно заранее. Так как код из неё выполняется в рамках кода из которого вызван, это означает, что компилятор не может оптимизировать окружающий контекст. Браузеру приходится откладывать интерпретацию большей части окружающего кода до момента выполнения. Это добавляет значительное количество времени к работе.

Конструктор Function не настолько плох, как eval, так как не влияет на окружающий код, но всё равно он весьма медлителен.

Заменяем eval

Функция eval не только вредна — в большинстве случаев она просто не нужена. Часто она используется, потому что какая-то информация представлена в виде строки и, считается, что eval единственный способ использовать эту информацию. Следующий пример показывает распространённую ошибку:

function getProperty(oString) {
  var oReference;
  eval('oReference = test.prop.'+oString);
  return oReference;
}

А этот код делает тоже самое, но без использования eval:

function getProperty(oString) {
  return test.prop[oString];
}

Код, не использующий eval, выполняется приблизительно на 95% быстрее в Opera 9, Firefox и Internet Explorer, и приблизительно на 85% быстрее в Safari (заметьте, что при этом не учитывается время на непосредственный вызов функции).

Если вам нужна функция, используйте function

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

function addMethod(oObject,oProperty,oFunctionCode) {
  oObject[oProperty] = new Function(oFunctionCode);
}
addMethod(myObject,'rotateBy90','this.angle=(this.angle+90)%360');
addMethod(myObject,'rotateBy60','this.angle=(this.angle+60)%360');

А следующий делает тоже самое, только без Function. В нём используется создание анонимных функций, на которые можно ссылаться, как на обычные объекты:

function addMethod(oObject,oProperty,oFunction) {
  oObject[oProperty] = oFunction;
}
addMethod(myObject,'rotateBy90',function () { this.angle=(this.angle+90)%360; });
addMethod(myObject,'rotateBy60',function () { this.angle=(this.angle+60)%360; });

Избегайте использования with

Хотя with часто может быть удобен, он может плохо влиять на скорость. With заставляет движок каждый раз создавать дополнительный контекст, содержащий поля используемой переменной. Само это приводит к незначительным замедлениям. Одако, так как структура переменной не известна во время компиляции, компилятор не может нормально оптимизировать код.

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

Рассмотрим код:

with( test.information.settings.files ) {
  primary = 'names';
  secondary = 'roles';
  tertiary = 'references';
}

Вот так будет более эффективно:

var testObject = test.information.settings.files;
testObject.primary = 'names';
testObject.secondary = 'roles';
testObject.tertiary = 'references';

Не используйте try-catch-finally внутри критических функций

Конструкция try-catch-finally является довольно уникальной. В отличии от других, она создаёт переменную в текущем контексте во время выполнения. Это происходит при каждом выполнении раздела catch — объект исключения записывается в переменную. Эта переменная недоступна вне раздела catch. Она создаётся в начале выполнения catch и уничтожается при его завершении.

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

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

var oProperties = ['first','second','third',...,'nth'], i;
for( i = 0; i < oProperties.length; i++ ) {
  try {
    test[oProperties[i]].someproperty = somevalue;
  } catch(e) {
    ...
  }
}

В большинстве случаев конструкция try-catch-finally может быть перенесена так, чтобы окружить цикл. Хотя это несколько изменит семантику — после исключения цикл будет остановлен, хотя выполнения сценария продолжится.

var oProperties = ['first','second','third',...,'nth'], i;
try {
  for( i = 0; i < oProperties.length; i++ ) {
    test[oProperties[i]].someproperty = somevalue;
  }
} catch(e) {
  ...
}

Иногда, конструкции try-catch-finally можно избежать совсем, проверяя наличия свойства или с помощью другого подобного теста:

var oProperties = ['first','second','third',...,'nth'], i;
for( i = 0; i < oProperties.length; i++ ) {
  if( test[oProperties[i]] ) {
    test[oProperties[i]].someproperty = somevalue;
  }
}

Изолируйте eval и with

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

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

Хотя это не столь важно, но конструкция try-catch-finally может иметь своё нехорошее воздействие в некоторых браузерах, включая Оперу, поэтому лучше изолировать и её.

Избегайте глобальных переменных

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

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

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

Во-вторых, глобальные переменные являются свойствами объекта window, что означает, что они находятся не в одном, а в двух контекстах. В глобальном контексте переменные хранятся связанные с названиями, в то время как в локальных контекстах используется предопределённый оптимизированный индекс. В результате глобальные переменные отнимают больше времени на их поиск.

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

Вот простой пример, где i и s являются глобальными переменными, а функция использует их:

var i, s = '';
function testfunction() {
  for( i = 0; i < 20; i++ ) {
    s += i;
  }
}
testfunction();

Альтернативная же версия выполняется несколько быстрее. В актуальных браузерах, включая Opera 9 и последние версии Internet Explorer, Firefox, Konqueror и Safari, увеличение скорости составляет примерно 30% (статья 2006 года).

function testfunction() {
  var i, s = '';
  for( i = 0; i < 20; i++ ) {
    s += i;
  }
}
testfunction();

Остерегайтесь неявных преобразований объектов

Литералы, такие как строки, числа и логические переменные в рамках ECMAScript могут быть представлены двумя способами. Каждый из них может быть, как простым значением, так и объектом. Например, строковое значение может быть создано просто обычным присвоением литерала: oString='какая-то строка', в то же время эквивалентный объект может быть создан с помощью oString = new String('какая-то строка').

Любые свойства и методы определены только для объекта String а не для простого значения (примитива). Когда вы ссылаетесь на свойство или метод примитивной строки, механизм ECMAScript должен неявно создать новый объект String с тем же самым значением, после чего вызвать нужный метод. Этот объект используется только один раз и при следующем запросе будет создан снова.

При выполнении следующего примера будет создан 21 объект String: для каждого раза, когда вычисляется размер строки и для каждого использования charAt():

var s = '0123456789';
for( var i = 0; i < s.length; i++ ) {
  s.charAt(i);
}

Эквивалентный код создаёт только один объект и выполняется быстрее:

var s = new String('0123456789');
for( var i = 0; i < s.length; i++ ) {
  s.charAt(i);
}

Если ваш код слишком часто обращается к методам примитивных значений – преобразуйте их в объекты.

Заметьте, что хотя большинство пунктов данной статьи относится ко всем браузерам, данная специфическая оптимизация, нацелена в основном на Оперу. Это так же может использоваться и в других браузерах, но может дать меньший эффект в Internet Explorer и Firefox.

Избегайте for-in в критических функциях

Для цикла for-in есть своё назначение, но часто он неправильно используется там, где мог быть использован обычный цикл. Цикл for-in требует от движка построение списка всех перечисляемых свойств и проверку дубликатов перед началом перечисления.

Очень часто, сценарий уже непосредственно знает, какие свойства должны быть перебраны. Во многих случаях может быть использован простой for. Особенно если перебираемые свойства именуются последовательными числами, например, array или объект, который имеет свойства для представления себя массивом (как DOM-объект NodeList).

Пример некорректного использования for-in:

var oSum = 0;
for( var i in oArray ) {
  oSum += oArray[i];
}

for здесь был бы более к месту:

var oSum = 0;
var oLength = oArray.length;
for( var i = 0; i < oLength; i++ ) {
  oSum += oArray[i];
}

Конкатентация строк

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

a += 'x' + 'y';

Этот код первым делом создаёт временную строку в памяти, связывает значение в «xy», затем складывает с текущим значением a и в конце концов связывает получившееся значение с переменной a. Следующий же код использует две отдельных команды, но при этом не используется временная переменная. Получившийся код примерно на 20% быстрее во многих браузерах и потенциально требует меньше памяти за счёт отсутствия временной строки.

a += 'x';
a += 'y';

Примитивные операции могут быть быстрее вызовов функций

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

var min = Math.min(a,b);
A.push(v);

Следующие операции будут быстрее:

var min = a < b ? a : b;
A[A.length] = v;

Используйте функции, а не строки в setTimeout() и setInterval()

Данные два метода похожи на eval. Если передана строка, то после указанной задержки, она так же как и в eval будет разобрана, со всеми связанными с этим издержками.

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

setInterval('updateResults()',1000);
setTimeout('x+=3;prepareResult();if(!hasCancelled){runmore();}',500);

В первом случае функцию можно указать непосредственно. Во втором, код может быть обёрнут в анонимную функцию:

setInterval(updateResults,1000);
setTimeout(function () {
  x += 3;
  prepareResult();
  if( !hasCancelled ) {
    runmore();
  }
},500);

Отметьте то, что время ожидания не может соблюдаться точно во всех случаях. Вообще, браузеры займут немного больше, чем от них требовалось. Некоторые могут компенсировать это с setInterval, запуская в следующий раз функцию немного раньше. Другие просто будут пытаться добиваться правильного времени каждый раз. На подобные задержки влияет множество факторов – скорость процессора, параллельные процессы, загрузка JavaScript. Большинство браузеров неспособно выдержать задержку в 0ms, а минимальная задержка может быть от 10 до 100 ms.

22 комментария »

  • В целом полезно, но местами таки сильно напоминает случай с «что сильнее print или echo» :)

    dallone, 10.03.2009, 11:07

  • Лучше про прототипы расскажи :)

    adw0rd, 13.03.2009, 12:35

  • А что с прототипами?

    dallone, 24.03.2009, 22:20

  • Рассказать насколько они охеренны и как устроено все…

    adw0rd, 24.03.2009, 23:21

  • >>>на этапе компиляции
    яваскрипт не компилируется а транслируется в байткод

    >>>параллельные процессы
    в яваскрипте нет паралельных процессов
    http://mabp.kiev.ua/2008/03/24/multi_threaded_javascript/

    CTAPbIu_MABP, 26.03.2009, 22:29

  • >>>яваскрипт не компилируется а транслируется в байткод
    «переводит» в байткод :) Возможно, лучше было употребить на его месте «трансляция». Хотя в источнике «compilation».

    >>>в яваскрипте нет паралельных процессов
    Имеются ввиду параллельные процессы в операционке.

    vasa_c, 27.03.2009, 12:50

  • если бы была компиляция, компилятор выдавал ошибки еще до исполнения

    CTAPbIu_MABP, 27.03.2009, 12:55

  • хотя может опера и компелирует

    CTAPbIu_MABP, 27.03.2009, 12:57


  • alert(1);
    function ({)}
    alert(2);

    Компилятор (транслятор) выдаст ошибку до исполнения и первый алерт не сработает, хотя ошибка после него.

    vasa_c, 27.03.2009, 13:28

  • это ошибка синтаксиса, а не логики.
    ты ведь можешь создать несуществующий класс
    alert(1);
    function x (){
    return new NonExistingClass();
    }
    alert(2);

    CTAPbIu_MABP, 27.03.2009, 13:59

  • Да, но это не может быть ошибкой в JS до исполнения.

    vasa_c, 27.03.2009, 14:09

  • правильно, а компилятор бы ругался

    CTAPbIu_MABP, 27.03.2009, 14:13

  • Ругаться нужно не в зависимости от того компилятор это или транслятор, а в зависимости от языка. В JS это не ошибка до момента исполнения, поэтому и ругаться не следует.

    Хотя если понимать «компилятор» в узком смысле — транслятор в машинные коды, то да, в JS это не компилятор, а транслятор в байт-код + интерпретатор оного.

    vasa_c, 27.03.2009, 14:20

  • >>если бы была компиляция, компилятор выдавал ошибки еще до исполнения

    А типа Java SE транслятор в байт код не выдает ошибки до исполнения? Зависит от языка однако :P

    dallone, 9.04.2009, 12:59

  • dallone, я немного работал с Java SE на платформе андроид, ругается при компиляции.

    CTAPbIu_MABP, 9.04.2009, 13:03

  • Олег, Louieze — спаммер

    adw0rd, 22.06.2009, 17:44

  • Ясно, только как оно просочилось?

    vasa_c, 22.06.2009, 19:24

  • Я не знаю как ты защищаешься :)
    Раньше у меня были такие посты, но теперь их нет.

    adw0rd, 22.06.2009, 20:54

  • Что бы там ни писали «умные» авторы этой дурацкой оперы (уже по тому, что они назвали транслятор компилятором, видно, что они очень много знают в программировании; собственно, это видно по их продукту, хотя кто знает, может они реально компилятор в оперу встроили xDDD ), а я провел собственные тесты на разных браузах с повтором в 100000 раз. Информация по соответствующим пунктам:
    1) Разбор строки с кодом это не такая и трудоёмкая задача.
    Для FF эвал замедляет скрипт в 4-8 раз.
    Opera — 5-9 раз.
    IE — 8-12 раз.
    Надо сказать, что все зависит и от длины запускаемой строки. Каждый новый оператор незначительно замедляет разбор.
    Где мои обещанные сотни раз?
    2) Результаты оказались неоднозначными.
    Для нормальных браузеров всё произошло, как описано выше. А для IE всё оказалось с точностью до наоборот.
    Отличия составили 10-20%.
    И как бы то ни было, оперовцы всё равно предлагают говнячий способ доступа к свойствам.
    Мой способ состоит в задействовании дополнительной переменной:
    var temp=document.body.firstchild.firstChild.nextSibling.style;
    temp.width=»100px»;
    temp.height=»100px»;
    …, чем каждый раз заново прописывать путь к объекту, это сокращает и время выполнения, и сам код.
    И т.п. Этот способ оказылся действительно быстрее, чем заявленный оперовцами;
    3) Тут оперовцы правы и замедление составляет в зависимости от браузера 2-55%;
    5) 25-50%;
    6) Тут оперовцы облажались со своим продуктом, потому что это ускоряет только его, а все остальные браузы замедляет. Ускорение на 20%. Замедление 2-5 раз.
    8) Тоже лажа написана. Если использовать +=, то этот способ медленне примерно на 20%. И даже в опере.
    9) Оперовцы правы. С фокус с пушем даёт выигрыш на 20%.
    10) Оперовцы правы. В данном случае эвал может дать очень значительный проигрыш. Лень было протестить =).

    Не стесняйтесь критиковать (или еще жестче) меня и/или оперовцев ;)

    PASHAK, 12.04.2010, 16:32

  • *тфу мля: интерпретатор компилятором
    xDDD

    PASHAK, 12.04.2010, 16:34

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

    >1) Разбор строки с кодом это не такая и трудоёмкая задача.

    Разница даже в ваши 10 раз, имхо более трудоёмка.
    Там же не написано, что нефег вообще eval() использовать, написано, что обычно если подумать, находятся более изящные способы и без него.

    >И как бы то ни было, оперовцы всё равно предлагают говнячий способ доступа к свойствам.
    >Мой способ состоит в задействовании дополнительной переменной:

    Э-э… приглядитесь, они предлагают тоже самое

    Что-то я дальше запутался в нумерации пунктов :)

    vasa_c, 13.04.2010, 10:56

  • На эту тему есть интересная книга «JavaScript оптимизация производительности» Николас Закас

    Разработчик, 7.08.2014, 9:46

Leave a comment