4 мин
22K
Картинка, конечно, стронгли анрилейтед

Разные трюки я тестировал на Google Chrome 107.0.5304.107 и Mozilla Firefox 107.0 на Windows 10.

Чтобы результаты всегда были железно воспроизводимыми, я отключил все С-State’ы, ядра зафиксировал на 5 ГГц.

У меня 9900К, это Coffee Lake c AVX256, какие оптимизации применит Jit для вашего процессора — я не знаю, результат на вашем компьютере может отличаться от моего, в т.ч. из-за микроархитектуры процессора.

Скорость парсинга кода тоже входит в бенчмарк, поэтому браузер с быстрым парсером будет впереди.

Есть ли у переменной оверхед?


Есть ли смысл использовать только dot notation? Какова цена выноса лишней переменной?

var array = new Array(65535).fill()


var a = array.map((x) => x)
var b = array.map((x) => x)
var c = array.map((x) => x)


var a = array.map((x) => x)
var b = array.map((x) => x).map((x) => x)


var a = array.map((x) => x).map((x) => x).map((x) => x)

Чтобы узнать, гоняет ли браузер память туда-сюда, делаем мы .map на массив длиной 65535 с нулями внутри. Линк.

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

Хром не заметил разницы, а вот лиса заметила. Применительно к лисе, у лишней переменной есть измеряемый оверхед.

Есть ли разница между var, let, const или их отсутствием?



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

var g = { e: [] }
g.o = function(x) { g.e.push(...[1,2,3]) }
g.o()

Код выглядит так, отличаются только биндинги.


Результат неожиданный, но железно воспроизводимый. var, быстрее.

Bounce pattern, Switch case, длинная тернарка


Если обе конструкции логически одинаковые, они должны строить одно и то же синтаксическое дерево, верно? Давайте проверим.


function thing(e) {
    switch (e) {
      case 0:
        return "0";

      case 1:
        return "1";

      case 2:
        return "2";

      case 3:
        return "3";
        
      default:
        return "";
    }
}


function bounce(x)
{
   if (x === 0) return "0";
   if (x === 1) return "1";
   if (x === 2) return "2";
   if (x === 3) return "3";
   
   return ""
}


function bounce(x) {
  return 0 === x ? "0" : 1 === x ? "1" : 2 === x ? "2" : 3 === x ? "3" : "";
}

Вот так выглядит код. Для всех вариантов он одинаков, отличаются только вызовы.

▍ 1. Вызов в цикле

for (let t = 0; 1e5 > t; t++) bounce(0), bounce(2), bounce(6);

Вызов выглядит так. Линк.


▍ 2. В цикле с другим типом

for (let t = 0; 1e5 > t; t++) bounce("0"), bounce("2"), bounce("");

Тут мы покидываем строку вместо числа. В свитче и if блоках используется строгое равенство, поэтому свитч выходит только через default, а if’ы выходят только последний return. Линк.


▍ 3. Без цикла

bounce(0), bounce(2), bounce(6)

Просто три вызова подряд, никаких циклов. Линк.


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

Также лиса, похоже, не строит одно и то же AST, как это делает хром. Рекомендую заменить ваши длинные if’ы и bounce паттерны на свитчи, чтобы избежать лисиных тормозов.

Инициализация массива


Для примера возьму из паттернов функционального программирования, когда ты инициализируешь массив, прокидывая лямбду в инициализатор. Просто ради примера, в качестве этой лямбды будет fizzbuzz.

var times = 65535;

function initializer(val, z) {
    const i = z % 5 | 0;
    return 0 == (z % 3 | 0) ? 0 === i ? "fizzbuzz" : "fizz" : 0 === i ? "buzz" : z;
}


var b = new Array(times);
for (var i = 0; i < times; i++) {
    b[i] = initializer(b[i], i)
}
b


var c = [];
for (var i = 0; i < times; i++) {
    c.push(initializer(c[i], i))
}
c


new Array(times).fill().map(initializer)

Это не самый красивый fizzbuzz, но это мой fizzbuzz. Линк на бенч.


Вариант с fill map создаёт два массива, сначала при вызове конструктора, потом при вызове map. Но такой вариант безальтернативно быстрее на хроме.

Конкатенация массивов


arr.reduce((acc, val) => acc.concat(val), [])


arr.flatMap(x => x)


arr.flat()


arr.reduce((acc, val) => {
    if (val) val.forEach(a => acc.push(a));
    return acc;
}, [])


let acc = [];

arr.forEach(val => {
    val && val.forEach(v => acc.push(v));
}), acc;


[].concat(...arr)

Конкатенация массивов на 1 уровень, поведение идентичное flat(1). Линк.


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

Уничтожение хрома


Бенчмарки ниже я перепроверял по нескольку раз, результат одинаковый и верный. Лиса действительно такая быстрая.

▍ Итерация по массиву


Сравнивать будем Array.prototype.forEach vs for...of vs for. На код смотрите по линку.


Ради производительности, циклы for, лучше переделать в forEach, чтобы хром не отставал.

▍ Содержит ли строка значение


url.includes('matchthis')


/matchthis/.test(url)


url.match(/matchthis/).length >= 0


url.indexOf('matchthis') >= 0


url.search('matchthis') >= 0



Трюк с IndexOf быстрее и на лисе, и на хроме. Используйте трюк с IndexOf. Линк на бенчмарк.

Преобразование строки в число


Тестируем неявное преобразование, парсинг и вызов конструктора.


var imp = + strNum


var toStr = parseFloat(strNum)


var num = Number(strNum)



Линк на бенч.

▍ Float




Я перепроверял, это не ошибка. Неявный каст стринги в инт практически бесплатный у лисы. Линк на бенч.

Выводы

  1. Лисичка похорошела.
  2. JS сделан за неделю на коленке.
  3. Я не пишу на JS.
  4. Вы тоже прекращайте.