Как сделать ваш javascript быстрее / Роман Дворнов (Авито)

Post on 06-Jan-2017

249 Views

Category:

Engineering

2 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Как сделать ваш 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

Хорошее начало – блог и доклады Вячеслава Егорова

mrale.ph/blog/

101

Блоги браузеров – ценный источник информации

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

Получаем данные о работе кода

106

mrale.ph/irhydra/2/

code.asm code.cfg

Заключение

Без понимания того, как устроены 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

Роман Дворнов @rdvornov

github.com/lahmatiy rdvornov@gmail.com

Спасибо!

top related