JavaScript, который мы пишем, не всегда исполняется, как мы думаем. Виртуальные машины, исполняющие его, делают многое, чтобы он работал быстрее. Но они не всесильны, и чтобы сделать код действительно быстрым, нужно знать их особенности и как все работает под капотом.
Поговорим об этих особенностях, что может служить причиной потери производительности, как это диагностировать и как делать код действительно быстрым. Доклад базируется на опыте, полученном в ходе работы над такими проектами как basis.js (весьма быстрый фреймворк для SPA), CSSO (минификатор CSS, который из медленного стал один из самых быстрых), CSSTree (самый быстрый детальный CSS парсер) и других.
4. Производительность Frontend'а
• Не всегда проблема (и так быстро)
• Если работает медленно, не всегда это
связано с JavaScript (особенно в браузере)
• Доклад про те ситуации, когда проблема
действительно в JavaScript
4
6. Простого ответа нет
• Нужно разбирать каждый случай отдельно
• Пара символов или строк могут изменить
производительность в разы или даже в десятки раз
• На производительность могут влиять внешние факторы
• Тема производительности JavaScript все еще
не стабильна – все меняется
• Тема огромная, многие аспекты требуют предварительной
подготовки
6
7. В общем случае, нужно понимать как
работают JavaScript движки,
что фактически происходит под капотом,
принимать меры там, где это нужно
7
8. О чем поговорим
• Заблуждения
• Новое в JavaScript
• Внешнее влияние на производительность
• Что можно найти под капотом
8
14. switch vs. hasOwnProperty
14
function testSwitch(quality){
switch (quality) {
case "Hard Working":
case "Honest":
case "Intelligent":
case "Team player":
return true;
default:
return false;
}
}
var o = {
'Hard Working': true,
'Honest': true,
'Intelligent': true,
'Team player': true
};
function testHOP(quality) {
return o.hasOwnProperty(quality)
}
Нужно перебирать
все варианты – медленно
Быстрее и гибче
16. Значит switch быстрее hasOwnProperty?
• Не всегда, в данном случае – да
• В общем случае (в режиме интерпретатора)
обычно медленнее
• Время switch в примере обусловлено его
оптимизацией при компиляции
• В то же время, hasOwnProperty не оптимизируется
16
17. Намеренно деоптимизируем
17
try/catch не дает функции оптимизироваться (V8)
function testSwitch(quality){
try{}catch(e){}
switch (quality) {
case "Hard Working":
case "Honest":
case "Intelligent":
case "Team player":
return true;
default:
return false;
}
}
var o = {
'Hard Working': true,
'Honest': true,
'Intelligent': true,
'Team player': true
};
function testHOP(quality) {
try{}catch(e){}
return o.hasOwnProperty(quality)
}
19. Выводы
• switch работает быстро, если оптимизируется
• другой код может помешать оптимизации
• могут быть дополнительные ограничения:
например, ранее V8 не оптимизировал switch
если вариантов (case) более 128
19
21. for..in vs. Object.keys()
21
for (var key in object) {
// do something
}
for..in – плохо, потому что перебираются как
собственные ключи так и ключи в цепочке
прототипов
22. for..in vs. Object.keys()
22
for (var key in object) {
if (object.hasOwnProperty(key)) {
// do something
}
}
лучше проверять, что ключ является собственным,
но это дополнительная проверка
23. for..in vs. Object.keys()
23
var keys = Object.keys(object);
for (var i = 0; i < keys.length; i++){
// do something
}
Object.keys() возвращает только собственные
ключи – это лучше и быстрее
25. Разбираемся
25
for..in действительно перебирает как
собственные ключи так и ключи в цепочке
прототипов – это сложно оптимизировать и
стоит избегать
for (var key in object) {
// do something
}
26. Разбираемся
26
дополнительная проверка позволяет оптимизатору
распознать паттерн и сгенерировать код, который
не будет трогать цепочку прототипов
for (var key in object) {
if (object.hasOwnProperty(key)) {
// do something
}
}
27. Разбираемся
27
да, Object.keys() перебирает только собственные
ключи и это быстро, но в результате создается
временный массив, который нужно итерировать,
к тому же это создает нагрузку на GC
var keys = Object.keys(object);
for (var i = 0; i < keys.length; i++){
// do something
}
28. for..in vs. Object.key()
28
forIn: 170 ms
forInHOP: 56 ms
objectKeys: 188 ms
С оптимизацией
forIn: 202 ms
forInHOP: 232 ms
objectKeys: 244 ms
Без оптимизации
29. Выводы
• for..in в общем случае немного быстрее
• hasOwnProperty проверка может приводить
к лучшей оптимизации for..in
• Object.keys() может и отрабатывает быстрее,
но генерирует мусор и не оптимизируется
29
32. Оптимизация циклов
32
for (var i = 0, len = array.length; i < len; i++) {
// do something
}
нужно его ускорить, закешировав длину
массива, но и это не самый быстрый вариант
34. Тест автора статьи
34
var arr = [];
for (var i = 0; i <= 1000000; i++) {
arr.push(i);
}
console.time("slowLoop");
for (var k = 0, len = arr.length; k < len; k++) {
// do something
}
console.timeEnd("slowLoop");
console.time("fastLoop");
var j = arr.length;
while (j--) {
// do something
}
console.timeEnd("fastLoop");
36. На самом деле…
• В последних браузерах "slowLoop" обычно
быстрее "fastLoop"
• Временные интервалы малы, в таких случаях
велика погрешность
• Сам по себе тест неверный
36
37. Разбираемся
37
var arr = [];
for (var i = 0; i <= 1000000; i++) {
arr.push(i);
}
console.time("slowLoop");
for (var k = 0, len = arr.length; k < len; k++) {
// do something
}
console.timeEnd("slowLoop");
console.time("fastLoop");
var j = arr.length;
while (j--) {
// do something
}
console.timeEnd("fastLoop");
Изначально код не
оптимизуется – если код
выполняется лишь раз, нет
смысла оптимизировать
38. Разбираемся
38
var arr = [];
for (var i = 0; i <= 1000000; i++) {
arr.push(i);
}
console.time("slowLoop");
for (var k = 0, len = arr.length; k < len; k++) {
// do something
}
console.timeEnd("slowLoop");
console.time("fastLoop");
var j = arr.length;
while (j--) {
// do something
}
console.timeEnd("fastLoop");
Тело цикла выполняется
много раз и могло было бы
оптимизироваться, но
здесь оно пустое
39. Разбираемся
39
var arr = [];
for (var i = 0; i <= 1000000; i++) {
arr.push(i);
}
console.time("slowLoop");
for (var k = 0, len = arr.length; k < len; k++) {
// do something
}
console.timeEnd("slowLoop");
console.time("fastLoop");
var j = arr.length;
while (j--) {
// do something
}
console.timeEnd("fastLoop");
По сути сравнивается
время выполнения этих
инструкций
40. Выполним тест несколько раз
40
function test(){
console.time("slowLoop");
for (var k = 0, len = arr.length; k < len; k++) {
// do something
}
console.timeEnd("slowLoop");
console.time("fastLoop");
var j = arr.length;
while (j--) {
// do something;
}
console.timeEnd("fastLoop");
}
test();
test();
test();
42. Результаты
41
slowLoop: 3.00 ms
fastLoop: 2.07 ms
slowLoop: 0.85 ms
fastLoop: 1.38 ms
slowLoop: 1.14 ms
fastLoop: 1.57 ms
Первое исполнение без оптимизации
Последующие с оптимизацией
43. Промежуточные выводы
• Код оптимизируется по мере разогрева
• Простые функции оптимизируются на
втором-третьем вызове
• Оптимизированный код может поменять
картину
42
45. Поменяем подход к тестированию
44
function test(x){
loop {
x++;
}
return x;
}
console.time('test');
for (var i = 0, res = 0; i < 100; i++) {
res += test(i);
}
console.timeEnd('test');
• каждую функцию выполняем
несколько раз – даем
возможность оптимизациям
• добавляем одинаковую
полезную нагрузку –
увеличиваем время
выполнения уменьшаем
влияние погрешности
• избегаем dead code
elimination
47. Выводы
• while быстрее for – миф из прошлого
• для современных движков обычно нет
необходимости кешировать значения в циклах
• на скорость цикла больше влияет
оптимизация чем форма записи
46
49. Выводы
• Гипотезы нужно подтверждать тестами
• Часто код работает не так, как мы думаем
• Не стоит жить мифами, движки
эволюционируют – нужно освежать свои знания
• Микробенчмарки – зло, если создаются без
понимания работы движков
48
50. Советы
• Не стоит доверять всему, что пишут в интернетах
или говорят в докладах, перепроверяйте
• Наиболее точная информация в публикациях
разработчиков браузеров, движков и независимых
авторов, объясняющих почему именно так
• Смотрите на дату публикации, даже верные
утверждения могут устареть
49
54. Правда жизни
• Часто новые возможности реализуют по принципу
"чтобы работало" – без учета производительности
• Новые конструкции могут не оптимизироваться и
мешать оптимизации сопряженного кода
• Некоторые возможности из ES5/ES6/etc в
принципе не могут быть оптимизированы
и работать быстро
53
57. Однако, в V8 (Chrome/node.js)
let/const медленнее var в 2 раза,
в остальных движках время
одинаковое
56
jsperf.com/let-vs-var-performance/50
58. – Вячеслав Егоров
“... [const] это все-таки неизменяемая привязка
переменной к значению ...
С другой стороны виртуальная машина может и
должна бы использовать это самое свойство
неизменяемости ...
V8 не использует, к сожалению.”
57
habrahabr.ru/company/jugru/blog/301040/#comment_9622474
60. Два года назад, я решил узнать
насколько мой полифил для
Promise медленней нативной
реализации…
59
github.com/lahmatiy/es6-promise-polyfill
61. Тест №1
60
var a = []; // чтобы инстансы не разрушались/собирались GC
var t = performance.now();
for (var i = 0; i < 10000; i++)
a.push(new Promise(function(){}));
console.log(performance.now() - t);
62. Тест №2
61
var a = []; // чтобы инстансы не разрушались/собирались GC
var t = performance.now();
for (var i = 0; i < 10000; i++)
a.push(new Promise(function(r, rj){ a.push(r, rj) }));
console.log(performance.now() - t);
63. Promise – 2 года назад
62
gist.github.com/lahmatiy/d4d6316418fe349537dc
Test 1 Test 2
Native Polyfill Native Polyfill
Chrome 35 105 15 154 18
Firefox 30 90 17 113 25
IE11 – 5 – 6
время в миллисекундах
64. Promise – сегодня
63
Test 1 Test 2
Native Polyfill Native Polyfill
Chrome 54 12.5 5.8 13.7 8
Firefox 49 101 31 119.2 43.1
Edge 14 12.7 25.7 22.2 40.2
Safari 10 3.7 1.8 4.3 2.3
время в миллисекундах
65. Полифил Promise (не самый
быстрый) по прежнему быстрее
нативных реализаций
почти во всех движках/браузерах
64
66. Это афектит все Promise-based
API и новые фичи
вроде async/await
65
67. Я попытался еще ускорить
полифил Promise, например,
используя Function#bind вместо
замыканий…
66
70. Результаты – 2 года назад
69
gist.github.com/lahmatiy/3d97ee23f3d89941970f
Closure Function#bind
Chrome 35 14 28
Firefox 30 10.3 17.1
IE11 9.3 2.9
время в миллисекундах
71. Результаты – сегодня
70
Closure Function#bind
Chrome 54 2.5 0.8
Firefox 49 3.8 5.7
Edge 14 5.1 4.2
Safari 10 1.0 4.0
время в миллисекундах
78. Выводы
• Новое не всегда работает быстро
• Нужно время, чтобы в движки добавили
новые оптимизации и что-то заработало
быстро
77
79. Советы
• Все новое в JavaScript стоит проверять – работает
ли быстро, оптимизируется ли
• Стоит читать блоги/release notes разработчиков
движков и браузеров, в них пишут о добавлении
новых оптимизаций
• Критические к производительности места стоит
писать на ES3/ES5
78
84. Это не часть JavaScript, однако
API часто синхронное и время
его вызова прибавляется ко
времени выполнения JavaScript
83
85. Пример: DOM
84
function doSomething(el, viewport) {
el.style.width = viewport.offsetWidth + 'px';
el.style.height = viewport.offsetHeight + 'px';
}
С точки зрения JavaScript, здесь все просто
и нечего оптимизировать
86. Пример: DOM
85
function doSomething(el, viewport) {
el.style.width = viewport.offsetWidth + 'px';
el.style.height = viewport.offsetHeight + 'px';
}
Но для второго чтения потребуется сделать
пересчет layout'а (дорогая операция), так как
до этого был изменен DOM
87. Пример: DOM
86
function doSomething(el, viewport) {
var width = viewport.offsetWidth;
var height = viewport.offsetHeight;
el.style.width = width + 'px';
el.style.height = height + 'px';
}
В этом случае сначала делается чтение, потом
запись – код не тригирует пересчет layout'а
88. Стоит помнить
• Время выполнения внешних API добавляется
к JavaScript и останавливает его выполнение
• Не все, что доступно в JavaScript является
его частью
• Внешние API могут приводить к побочным
явлениям (side effect) затратным по времени
87
91. Выделение памяти
90
var array = [];
for (var i = 0; i < 1000; i++) {
array.push(i);
}
Плохо – может приводить к релокации
фрагментов памяти (массивы хранятся
одним фрагментом)
92. Выделение памяти
91
var array = new Array(1000);
for (var i = 0; i < 1000; i++) {
array[i] = i;
}
Лучше – может помочь избежать релокацию,
так как сразу выделится нужно кол-во памяти
93. Так же можно использовать структуры
данных, позволяющие избегать релокации,
например, TypedArray или списки
92
Подробнее в докладе:
Парсим CSS: performance tips & tricks
96. Влияние GC
95
> node --trace-gc test.js
...
[91494:0x102001000] 374 ms: Scavenge 35.3 (56.9) -> 35.0 (57.9) MB, 30.0 / 0.0 ms [allocation failure]
[91494:0x102001000] 443 ms: Scavenge 38.2 (59.9) -> 38.1 (74.9) MB, 46.2 / 0.0 ms [allocation failure]
===== run #1 152 ms
===== run #2 63 ms
===== run #3 44 ms
...
===== run #7 58
[91494:0x102001000] 896 ms: Scavenge 135.2 (159.9) -> 135.0 (160.9) MB, 31.5 / 0.0 ms [allocation fail
[91494:0x102001000] 965 ms: Scavenge 140.0 (163.9) -> 140.0 (178.9) MB, 59.2 / 0.0 ms [allocation fail
===== run #8 131 ms
===== run #9 43 ms
===== run #10 46 ms
97. Эволюция GC
• молодая и старая память
• инкрементальная сборка мусора
• параллельная сборка мусора
96
98. Простые советы
• Используем меньше памяти – быстрее
• Генерируем меньше мусора – быстрее
• Нужно понимать как происходит выделение
памяти и сборка мусора (GC)
97
100. Чтобы работать над ускорением
JavaScript, важно понимать как
устроены и работают JavaScript
движки
99
101. С чем стоит разобраться
• hidden class
• monomorphic, polymorphic, megamorphic
• inline cache
• function inlining
• dead code elimination
• tenuring
• ...
100
102. Хорошее начало – блог и
доклады Вячеслава Егорова
mrale.ph/blog/
101
104. Помимо этого
• Как работает железо (процессор, память –
регистры, адресация)
• Иметь преставление что такое машинный код
• Структуры данных (стек, etc)
• Как представляются структуры данных в
низкоуровневых языках (массивы, строки)
103
105. Самый верный способ узнать,
что на самом деле выполняет
движок – посмотреть внутреннее
представление
104
106. 105
node --trace-hydrogen
--trace-phase=Z
--trace-deopt
--code-comments
--hydrogen-track-positions
--redirect-code-traces
--redirect-code-traces-to=code.asm
--trace_hydrogen_file=code.cfg
--print-opt-code
your-script.js
Получаем данные о работе кода
110. Без понимания того, как
устроены JavaScript движки
крайне сложно писать
производительный код
109
111. Тема объемна – ее не постичь
за короткое время, потому
нужно понемногу в ней копаться
110
112. ВремясжатияCSS(600Kb)
500 ms
1 000 ms
1 500 ms
2 000 ms
2 500 ms
3 000 ms
3 500 ms
4 000 ms
4 500 ms
5 000 ms
5 500 ms
6 000 ms
Версия CSSO
1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 2.0
1 050 ms
clean-css
Оно того стоит: изменение скорости CSSO
csso
500 ms
cssnano
23 250 ms
113. 112
CSSTree: 7 ms
Mensch: 31 ms
CSSOM: 36 ms
PostCSS: 38 ms
Rework: 81 ms
PostCSS Full: 100 ms
Gonzales: 175 ms
Stylecow: 176 ms
Gonzales PE: 214 ms
ParserLib: 414 ms
Оно того стоит: CSSTree
github.com/postcss/benchmark
Разбор bootstrap.css v3.3.7 (146Kb)
Парсер CSSTree появился в результате
многочисленного рефакторинга Gonzales
Подробнее в докладе:
Парсим CSS: performance tips & tricks
114. Ищите объяснения, почему что-то
работает быстро или медленно –
тогда вы сами сможете ответить
на вопрос как сделать ваш
JavaScript быстрее
113