Пишем производительный JavaScript. 3 совета.


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


Помогите JavaScript понять, что на самом деле вы хотите сделать

Избегайте неопределённости. Например, если у вас есть значение obj, которое может быть undefined или объектом, рассмотрите возможность исключения неопределенности через явное условие:

if (obj !== undefined) {
// …
}

Тогда компилятору не придётся делать множество лишних проверок, как в случае, если вы напишите:

if (obj) {
// …
}

Что должен сделать компилятор в этом случае? Проверить, что obj не является пустой строкой, false, 0 или undefined. Всё это порождает лишние проверки в байткоде. Кажется, достаточно понимать, что перед нами объект, а объект всегда возвращает true на toBoolean. Но, к сожалению, в нашем динамически типизируемом языке компилятору придётся следить за всем циклом жизни переменной obj, чтобы убедиться, что на вход условия действительно пришёл объект.


Первый вариант в среднем на 15% быстрее

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

Осторожнее с && и ||


https://github.com/developit/preact/pull/610

Это пример из реального коммита в репозитории preact. Изменение логического выражения на тернарный оператор сделано для повышения производительности кода (автор сообщает об 1–5% ускорении). Но за счёт чего?

Мы помогли компилятору, указав, что значением vlen всегда будет Number. В негативном сценарии первого случая мы получим тип Boolean для vlen, что и приводит к деоптимизации, а так же добавляет ещё одну дорогостоящую проверку позже, чтобы убедиться, что vlen на самом деле число.

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

vlen = (vchildren !== undefined) ? vchildren.length : 0;

В целом, использование && или || в небулевом контексте (особенно с числами) не слишком хорошее решение, из-за семантики && и || в JavaScript.

Ещё один интересный пример неправильного использования ||:

function foo(a, b) {
a = a || "value";
b = b || 4;
// …
}

В данном случае ошибочно отсекаются допустимые значения, такие как пустая строка '' для a и 0 для b.

Не доверяйте undefined

Выше мы использовали проверку

if (obj !== undefined) {
// …
}

Но всё ли с ней хорошо? Давайте проведём небольшой эксперимент

const isDefined = (function() {
const undefined = 1;
return x => x !== undefined;
})();
console.log(isDefined(undefined)); // true
console.log(isDefined(1)); // false

На самом деле undefined не является ключевым словом в JavaScript. Это просто поле в глобальном объекте, и движок JS вынужден это учитывать. К счастью, в V8 уже присутствует неплохая оптимизация и если в цепочке видимости отсутствуют eval или with, компилятор не будет производить поиск значения undefined в глобальном объекте. Но вот в браузере Safari такая оптимизация отсутствует.

Защититься от подмены undefined (и немного упростить жизнь компилятору) можно путём вызова оператора void, который всегда возвращает настоящий undefined.

if (obj !== void 0) {
// …
}

Я не призываю использовать эту конструкцию, потому что она может поставить в тупик менее опытных коллег, столкнувшихся с вашим кодом. Но в случае необходимости достижения максимальной производительности стоит подумать о таком подходе. Также void 0 пригодится, если в вашем проекте включено правило no-undefined в ESlint.

Подробнее читайте в статье «Иногда undefined это defined»