Как сделать ваш JavaScript быстрее / Роман Дворнов (Авито)
-
Upload
ontico -
Category
Engineering
-
view
251 -
download
2
Transcript of Как сделать ваш JavaScript быстрее / Роман Дворнов (Авито)
Как сделать ваш JavaScript быстрее
Роман Дворнов Avito
Руководитель фронтенда в Avito
Основной интерес – SPA
Open source:basis.js, CSSO, component-inspector, csstree и другие
Вводная
Производительность Frontend'а
• Не всегда проблема (и так быстро)
• Если работает медленно, не всегда это связано с JavaScript (особенно в браузере)
• Доклад про те ситуации, когда проблема действительно в JavaScript
4
Как сделать JavaScript быстрее?
5
Простого ответа нет• Нужно разбирать каждый случай отдельно
• Пара символов или строк могут изменить производительность в разы или даже в десятки раз
• На производительность могут влиять внешние факторы
• Тема производительности JavaScript все еще не стабильна – все меняется
• Тема огромная, многие аспекты требуют предварительной подготовки
6
В общем случае, нужно понимать как работают JavaScript движки,
что фактически происходит под капотом, принимать меры там, где это нужно
7
О чем поговорим• Заблуждения
• Новое в JavaScript
• Внешнее влияние на производительность
• Что можно найти под капотом
8
Мифы и легенды
Разработчики руководствуются своими представлениями о том, что быстро и что нет – часто эти
представления не верны
10
4 Javascript Optimisations you should know
11
leftshift.io/4-javascript-optimisations-you-should-know
Пример вредной статьи
4 апреля 2014
12
Вредный совет #1
hasOwnProperty быстрее switch
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)}
Нужно перебирать все варианты – медленно
Быстрее и гибче
switch vs. hasOwnProperty
15
testSwitch: 4 mstestHOP: 40 ms
Простой тест показывает обратное
Значит switch быстрее hasOwnProperty?
• Не всегда, в данном случае – да
• В общем случае (в режиме интерпретатора) обычно медленнее
• Время switch в примере обусловлено его оптимизацией при компиляции
• В то же время, hasOwnProperty не оптимизируется
16
Намеренно деоптимизируем
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)}
Результаты
18
testSwitch: 70 mstestHOP: 42 ms
С оптимизацией
testSwitch: 4 mstestHOP: 40 ms
Без оптимизации (try/catch)
Выводы
• switch работает быстро, если оптимизируется
• другой код может помешать оптимизации
• могут быть дополнительные ограничения: например, ранее V8 не оптимизировал switch если вариантов (case) более 128
19
Вредный совет #2
for..in vs. Object.keys()
for..in vs. Object.keys()
21
for (var key in object) { // do something}
for..in – плохо, потому что перебираются как собственные ключи так и ключи в цепочке
прототипов
for..in vs. Object.keys()
22
for (var key in object) { if (object.hasOwnProperty(key)) { // do something }}
лучше проверять, что ключ является собственным, но это дополнительная проверка
for..in vs. Object.keys()
23
var keys = Object.keys(object);
for (var i = 0; i < keys.length; i++){ // do something}
Object.keys() возвращает только собственные ключи – это лучше и быстрее
for..in vs. Object.keys()
24
forIn: 170 msforInHOP: 56 msobjectKeys: 188 ms
Результаты теста (V8)
jsfiddle.net/rdvornov/veeorm09/
Разбираемся
25
for..in действительно перебирает как собственные ключи так и ключи в цепочке прототипов – это сложно оптимизировать и
стоит избегать
for (var key in object) { // do something}
Разбираемся
26
дополнительная проверка позволяет оптимизатору распознать паттерн и сгенерировать код, который
не будет трогать цепочку прототипов
for (var key in object) { if (object.hasOwnProperty(key)) { // do something }}
Разбираемся
27
да, Object.keys() перебирает только собственные ключи и это быстро, но в результате создается временный массив, который нужно итерировать,
к тому же это создает нагрузку на GC
var keys = Object.keys(object);
for (var i = 0; i < keys.length; i++){ // do something}
for..in vs. Object.key()
28
forIn: 170 msforInHOP: 56 msobjectKeys: 188 ms
С оптимизацией
forIn: 202 msforInHOP: 232 msobjectKeys: 244 ms
Без оптимизации
Выводы
• for..in в общем случае немного быстрее
• hasOwnProperty проверка может приводить к лучшей оптимизации for..in
• Object.keys() может и отрабатывает быстрее, но генерирует мусор и не оптимизируется
29
Вредный совет #3
Оптимизация циклов
Оптимизация циклов
31
for (var i = 0; i < array.length; i++) { // do something}
обычный цикл, который чем то не угодил
Оптимизация циклов
32
for (var i = 0, len = array.length; i < len; i++) { // do something}
нужно его ускорить, закешировав длину массива, но и это не самый быстрый вариант
Оптимизация циклов
33
var i = array.length;while (i--) { //do something}
while цикл быстрее for
Тест автора статьи
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");
Результаты теста
35
slowLoop: 3.47 msfastLoop: 2.52 ms
На самом деле…
• В последних браузерах "slowLoop" обычно быстрее "fastLoop"
• Временные интервалы малы, в таких случаях велика погрешность
• Сам по себе тест неверный
36
Разбираемся
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
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
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
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();
Результаты
41
slowLoop: 3.00 msfastLoop: 2.07 msslowLoop: 0.85 msfastLoop: 1.38 msslowLoop: 1.14 msfastLoop: 1.57 ms
Результаты
41
slowLoop: 3.00 msfastLoop: 2.07 msslowLoop: 0.85 msfastLoop: 1.38 msslowLoop: 1.14 msfastLoop: 1.57 ms
Первое исполнение без оптимизации
Последующие с оптимизацией
Промежуточные выводы
• Код оптимизируется по мере разогрева
• Простые функции оптимизируются на втором-третьем вызове
• Оптимизированный код может поменять картину
42
Так как же быстрее всего?
43
Поменяем подход к тестированию
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
Результаты
45
for: 155msforCache: 156mswhile: 183ms
С оптимизацией
for: 494msforCache: 460mswhile: 605ms
Без оптимизации
Выводы
• while быстрее for – миф из прошлого
• для современных движков обычно нет необходимости кешировать значения в циклах
• на скорость цикла больше влияет оптимизация чем форма записи
46
Подводим итоги
Выводы• Гипотезы нужно подтверждать тестами
• Часто код работает не так, как мы думаем
• Не стоит жить мифами, движки эволюционируют – нужно освежать свои знания
• Микробенчмарки – зло, если создаются без понимания работы движков
48
Советы• Не стоит доверять всему, что пишут в интернетах или говорят в докладах, перепроверяйте
• Наиболее точная информация в публикациях разработчиков браузеров, движков и независимых авторов, объясняющих почему именно так
• Смотрите на дату публикации, даже верные утверждения могут устареть
49
Новое не всегда хорошо
JavaScript развивается – появляются новые удобные конструкции, но не стоит
забывать о производительности
51
Поддержка со стороны движка не означает, что это работает
быстро
52
Правда жизни• Часто новые возможности реализуют по принципу
"чтобы работало" – без учета производительности
• Новые конструкции могут не оптимизироваться и мешать оптимизации сопряженного кода
• Некоторые возможности из ES5/ES6/etc в принципе не могут быть оптимизированыи работать быстро
53
var vs. let/const
Сегодня стало "модно" везде заменять var на let или const
55
Однако, в V8 (Chrome/node.js) let/const медленнее var в 2 раза,
в остальных движках время одинаковое
56
jsperf.com/let-vs-var-performance/50
– Вячеслав Егоров
“... [const] это все-таки неизменяемая привязка переменной к значению ...
С другой стороны виртуальная машина может и должна бы использовать это самое свойство неизменяемости ...
V8 не использует, к сожалению.”
57
habrahabr.ru/company/jugru/blog/301040/#comment_9622474
Promise
Два года назад, я решил узнать насколько мой полифил для Promise медленней нативной
реализации…
59
github.com/lahmatiy/es6-promise-polyfill
Тест №1
60
var a = []; // чтобы инстансы не разрушались/собирались GCvar t = performance.now();
for (var i = 0; i < 10000; i++) a.push(new Promise(function(){})); console.log(performance.now() - t);
Тест №2
61
var a = []; // чтобы инстансы не разрушались/собирались GCvar 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);
Promise – 2 года назад
62
gist.github.com/lahmatiy/d4d6316418fe349537dc
Test 1 Test 2Native Polyfill Native Polyfill
Chrome 35 105 15 154 18
Firefox 30 90 17 113 25
IE11 – 5 – 6
время в миллисекундах
Promise – сегодня
63
Test 1 Test 2Native 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
время в миллисекундах
Полифил Promise (не самый быстрый) по прежнему быстрее
нативных реализаций почти во всех движках/браузерах
64
Это афектит все Promise-based API и новые фичи вроде async/await
65
Я попытался еще ускорить полифил Promise, например,
используя Function#bind вместо замыканий…
66
closure vs. Function#bind
По идее Function#bind должен быть дешевле (быстрее)
68
Результаты – 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
время в миллисекундах
Результаты – сегодня
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
время в миллисекундах
Метод Function#bind все еще медленней (не оптимизирован) замыканий в ряде движков
71
Транспиляция
Транспиляция (например, ES6→ES5) уменьшает возможность влиять на код
и его производительность
73
Транспиляция может оказывать как положительный эффект, например,
оптимизация кода на основе статического анализа
74
Возможно и негативное влияние, когда сгенерированный код не может быть оптимизирован – в таких случаях
стоит переписать код на ES5/ES3
75
Подводим итоги
Выводы• Новое не всегда работает быстро
• Нужно время, чтобы в движки добавили новые оптимизации и что-то заработало быстро
77
Советы• Все новое в JavaScript стоит проверять – работает ли быстро, оптимизируется ли
• Стоит читать блоги/release notes разработчиков движков и браузеров, в них пишут о добавлении новых оптимизаций
• Критические к производительности места стоит писать на ES3/ES5
78
Беда может прийти откуда не ждешь
Даже если сам JavaScript работает быстро, внешние факторы могут значительно
влиять на его производительность
80
Внешние API
JavaScript код взаимодествует с внешними системами и API – таймеры, DOM, файловая
система, сеть и т.д.
82
Это не часть JavaScript, однако API часто синхронное и время его вызова прибавляется ко
времени выполнения JavaScript
83
Пример: DOM
84
function doSomething(el, viewport) { el.style.width = viewport.offsetWidth + 'px'; el.style.height = viewport.offsetHeight + 'px';}
С точки зрения JavaScript, здесь все просто и нечего оптимизировать
Пример: DOM
85
function doSomething(el, viewport) { el.style.width = viewport.offsetWidth + 'px'; el.style.height = viewport.offsetHeight + 'px';}
Но для второго чтения потребуется сделать пересчет layout'а (дорогая операция), так как
до этого был изменен DOM
Пример: 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'а
Стоит помнить• Время выполнения внешних API добавляется к JavaScript и останавливает его выполнение
• Не все, что доступно в JavaScript является его частью
• Внешние API могут приводить к побочным явлениям (side effect) затратным по времени
87
Память
Говоря о производительности JavaScript, часто забывают
о важном компоненте – памяти
89
Выделение памяти
90
var array = [];for (var i = 0; i < 1000; i++) { array.push(i);}
Плохо – может приводить к релокации фрагментов памяти (массивы хранятся
одним фрагментом)
Выделение памяти
91
var array = new Array(1000);for (var i = 0; i < 1000; i++) { array[i] = i;}
Лучше – может помочь избежать релокацию, так как сразу выделится нужно кол-во памяти
Так же можно использовать структуры данных, позволяющие избегать релокации,
например, TypedArray или списки
92
Подробнее в докладе: Парсим CSS: performance tips & tricks
GC может все испортить
93
94
Пример
Влияние 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 failure].[91494:0x102001000] 965 ms: Scavenge 140.0 (163.9) -> 140.0 (178.9) MB, 59.2 / 0.0 ms [allocation failure].===== run #8 131 ms===== run #9 43 ms===== run #10 46 ms
Эволюция GC• молодая и старая память
• инкрементальная сборка мусора
• параллельная сборка мусора
96
Простые советы• Используем меньше памяти – быстрее
• Генерируем меньше мусора – быстрее
• Нужно понимать как происходит выделение памяти и сборка мусора (GC)
97
Лезем под капот
Чтобы работать над ускорением JavaScript, важно понимать как устроены и работают JavaScript
движки
99
С чем стоит разобраться• hidden class
• monomorphic, polymorphic, megamorphic
• inline cache
• function inlining
• dead code elimination
• tenuring
• ...
100
Блоги браузеров – ценный источник информации
102
Помимо этого• Как работает железо (процессор, память – регистры, адресация)
• Иметь преставление что такое машинный код
• Структуры данных (стек, etc)
• Как представляются структуры данных в низкоуровневых языках (массивы, строки)
103
Самый верный способ узнать, что на самом деле выполняет
движок – посмотреть внутреннее представление
104
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
Получаем данные о работе кода
Заключение
Без понимания того, как устроены JavaScript движки
крайне сложно писать производительный код
109
Тема объемна – ее не постичь за короткое время, потому
нужно понемногу в ней копаться
110
Врем
я сж
атия
CSS
(600
Kb)
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 msclean-css
Оно того стоит: изменение скорости CSSO
csso500 ms
cssnano23 250 ms
112
CSSTree: 7 msMensch: 31 msCSSOM: 36 msPostCSS: 38 msRework: 81 msPostCSS Full: 100 msGonzales: 175 msStylecow: 176 msGonzales PE: 214 msParserLib: 414 ms
Оно того стоит: CSSTree
github.com/postcss/benchmark
Разбор bootstrap.css v3.3.7 (146Kb)
Парсер CSSTree появился в результате многочисленного рефакторинга Gonzales
Подробнее в докладе: Парсим CSS: performance tips & tricks
Ищите объяснения, почему что-то работает быстро или медленно – тогда вы сами сможете ответить
на вопрос как сделать ваш JavaScript быстрее
113