Js, трюки, наблюдения, бенчмарки и как Лиса уничтожает Хром. Я протестировал всё, что вам было лень

aiz5gztttvvkegk_annvvgtumv8.png

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

Разные трюки я тестировал на 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()

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

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

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


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

jwuxasvbrmoo8bd08xz23xqzbmk.jpeg


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

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


98bqpyb07ylqtmillpqva2ol6d8.jpeg


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

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


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

llt8wc8317gsft-b-3hzsquks-u.jpeg


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

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


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

// 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 "";
    }
}

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

// ternary
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);


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

5zc4anssaguhuexso36fsymxo1w.jpeg


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

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


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

cea5mooydemdi4kzpf2v8wtphjs.jpeg


▍ 3. Без цикла

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


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

8-i5p4n-v3fvcckb41o7aue2a-m.jpeg


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

Также лиса, похоже, не строит одно и то же 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;
}

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

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

// Fill Map
new Array(times).fill().map(initializer)


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

2umsat5-a9manougryyx3iohjhq.jpeg


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

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


8kuqb0dhvsvodnu0djtqvmn_aoe.jpeg
// reduce
arr.reduce((acc, val) => acc.concat(val), [])

// flatMap
arr.flatMap(x => x)

// flat
arr.flat()

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

// forEach push
let acc = [];

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

//concat spread
[].concat(...arr)


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

ey_-hsu2gbykaalxdwgvpg_itr0.jpeg


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

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


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

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


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

pouefzncq6xggwoxqyfnoy38wng.jpeg


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

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

// text.includes()
url.includes('matchthis')

// text.test()
/matchthis/.test(url)

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

// text.indexOf()
url.indexOf('matchthis') >= 0

// text.search()
url.search('matchthis') >= 0
4f2l4-2wfg-pabfjrpgom4yx_ny.jpeg


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

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


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

// implicit
var imp = + strNum

// parseFloat
var toStr = parseFloat(strNum)

//Number
var num = Number(strNum)


▍ Int

8b0grbqwue2slct2x4gc95sy4nc.jpeg


Линк на бенч.

▍ Float

8b0grbqwue2slct2x4gc95sy4nc.jpeg


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

Выводы


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


Играй в нашу новую игру прямо в Telegram!

sz7jpfj8i1pa6ocj-eia09dev4q.png

© Habrahabr.ru