Пишемо швидкий і економний код на JavaScript

Такий движок JS, як V8 (Chrome, Node) від Google, заточений для швидкого виконання великих додатків. Якщо ви під час розробки дбаєте про ефективне використання пам'яті і швидкодії, вам необхідно знати дещо про процесах, що проходять в движку JS браузера.

Що б там не було — V8, SpiderMonkey (Firefox), Carakan (Opera), Chakra (IE) або щось ще, знання внутрішніх процесів допоможе вам оптимізувати роботу ваших додатків. Але не закликаю вас оптимізувати движок для одного браузера або движка — не робіть так.

Задайте собі питання:
— чи можна щось у моєму коді зробити більш ефективним?
— яку проводять оптимізацію популярні движки JS?
— що движок не може компенсувати, і може збірка сміття підчистити все так, як я від неї чекаю?



Є багато пасток, пов'язаних з ефективним використанням пам'яті і швидкодією, і в статті ми вивчимо деякі підходи, які добре показали себе в тестах.

І як же JS працює в V8?

Хоча можливо розробляти великі додатки без належного розуміння роботи движка JS, будь-який автовласник скаже вам, що він хоч раз заглядав під капот автомобіля. Оскільки мені подобається Chrome, я розповім про його JavaScript-движок. V8 складається з декількох основних частин.

Основний компілятор, який обробляє JS і видає машинний код перед його виконанням, замість того, щоб виконувати байткод або просто інтерпретувати його. Цей код зазвичай не сильно оптимізовано.
— V8 перетворює об'єкти у об'єктну модель. В JS об'єкти реалізовані як асоціативні масиви, але в V8 вони представлені прихованими класами, які є внутрішньою системою типів для оптимізованого пошуку.
профайлер часу виконання, який відстежує роботу системи і визначає «гарячі» функції (код, який довго виконується)
оптимізуючий компілятор, який рекомпилирует і оптимізує гарячий код, і займається іншими оптимизациями, начебто инлайнинга
— V8 підтримує деоптимизацию, коли оптимізуючий компілятор робить відкат, якщо він виявляє, що він зробив якісь занадто оптимістичні припущення при розборі код
збірка сміття. Уявлення про її роботу так само важливо, як уявлення про оптимізації.

Збірка сміття

Це одна з форм управління пам'яттю. Складальник намагається повернути пам'ять, зайняту об'єктами, які вже не використовуються. У мові з підтримкою сміття, об'єкти, на які ще є посилання, не підчищаються.

Майже завжди можна не видаляти посилання на об'єкти вручну. Просто розміщуючи змінні там, де вони потрібні (в ідеалі, ніж локальніше, тим краще — всередині функцій, які їх використовують, а не у зовнішній області видимості), можна добитися нормальної роботи.



В JS не можна насильно змусити працювати збирання сміття. Це і не потрібно робити, тому що цей процес контролюється під час виконання, і йому видніше, коли і що підчищати.

Помилки з видаленням посилань на об'єкти
У деяких спорах в онлайні з приводу повернення пам'яті в JS виникає ключове слово delete. Хоча спочатку воно призначається для видалення ключів, деякі розробники вважають, що з його допомогою можна провести примусове видалення посилань. Уникайте використання delete. У прикладі нижче delete o.x приносить більше шкоди, ніж користі, оскільки змінює прихований клас у o і робить його повільним об'єктом.

var o = { x: 1 }; 
delete o.x; // true 
o.x; // undefined


Ви обов'язково знайдете відсилання до delete в багатьох популярних JS-бібліотеках, оскільки в ньому є сенс. Головне, що потрібно засвоїти — не потрібно змінювати структуру «гарячих» об'єктів під час виконання програми. Движки JS можуть розпізнавати такі «гарячі» об'єкти і пробувати оптимізувати їх. Це буде простіше зробити, якщо структура об'єкта не сильно змінюється, а delete якраз призводить до таких змін.

Є і нерозуміння з приводу того, як працює null. Установка посилання на об'єкт у null не обнуляє об'єкт. Писати o.x = null краще, ніж використовувати delete, але сенсу це не має.

var o = { x: 1 }; 
o = null;
o; // null
o.x // TypeError


Якщо ця посилання була останньою посиланням на об'єкт, його потім прибере збирач сміття. Якщо це була не остання посилання, до нього можна дістатися, і збирач його не підбере.

Ще одне зауваження: глобальні змінні не прибираются складальником сміття, поки працює сторінка. Неважливо, як довго вона відкрита, змінні з області видимості глобального об'єкта будуть існувати.

var myGlobalNamespace = {};


Глобальні змінні підчищаються при перезавантаженні сторінки, перехід до іншої сторінки, закриття закладки або виходу з браузера. Змінні з області видимості функції підчищаються, коли пропадає область видимості — коли функція здійснює вихід, і на них більше немає посилань.

Прості правила
Щоб збірка сміття спрацювала так рано, як це можливо, і зібрала якомога більше об'єктів, не тримайтеся за ті об'єкти, які вам не потрібні. Зазвичай це відбувається автоматично, але ось про що необхідно пам'ятати:

— Хорошою альтернативою ручного знищення посилань є використання змінних з правильною областю видимості. Замість присвоювання глобальної змінної null використовуйте локальну для функції змінну, яка зникає, коли зникає область видимості. Код стає чистішим, і виникає менше турбот.
— Переконайтеся, що ви знімаєте обробники подій, коли вони вже не потрібні, особливо перед видаленням елементів DOM, до яких вони прив'язані.
— При використанні локального кеша даних переконайтеся, що ви очистили його, або використовували механізм старіння, щоб не зберігати великі непотрібні шматки даних.

Тепер звернемося до функцій. Як ми вже сказали, збірка сміття звільняє використовувалися блоки пам'яті (об'єкти), до яких вже не можна дістатися. Для ілюстрації кілька прикладів.

function foo() {
var bar = new LargeObject();
bar.someCall();
}


По поверненню з foo, об'єкт, на який вказує bar, буде почищено складальником сміття, оскільки на нього вже нічого не посилається.

Порівняйте з:

function foo() {
var bar = new LargeObject();
bar.someCall();
return bar;
}

// де ще в коді
var b = foo();


Тепер у нас є посилання на об'єкт, що зберігається, поки викликав функцію код не призначить в b що-небудь ще (або поки b не вийде з області видимості).

Замикання
Коли ви зустрічаєте функцію, яка повертає внутрішню функцію, то у внутрішній є доступ до області видимості поза її, навіть після закінчення роботи зовнішньої. Це і є замикання — вираз, який може працювати з змінними з обраного контексту. Наприклад:

function sum (x) {
function sumIt(y) {
return x + y;
};
return sumIt;
}

// Використання
var sumA = sum(4);
var sumB = sumA(3);
console.log(sumB); // Повертає 7


Збирач сміття не може прибрати створений функціональний об'єкт, оскільки до нього ще є доступ, наприклад через sumA(n). Ось ще приклад. Чи можемо ми отримати доступ до largeStr?

var a = function () {
var largeStr = new Array(1000000).join('x');
return function () {
return largeStr;
};
}();

Так — через а(), тому він теж не усувається складальником. Як щодо такого:

var a = function () {
var smallStr = 'x';
var largeStr = new Array(1000000).join('x');
return function (n) {
return smallStr;
};
}();


У нас вже немає до неї доступу, тому його можна підчищати.

Таймери
Одне з найгірших місць для витоків — цикл, або в парі setTimeout()/setInterval(), хоча така проблема зустрічається досить часто. Розглянемо приклад:

var myObj = {
callMeMaybe: function () {
var myRef = this;
var val = setTimeout(function () { 
console.log('Час виходить!'); 
myRef.callMeMaybe();
}, 1000);
}
};


Якщо ми виконаємо

myObj.callMeMaybe();


щоб запустити таймер, кожну секунду буде виводитися "'Час виходить!". Якщо ми виконаємо:

myObj = null;


таймер все одно продовжить роботу. myObj неможливо підчистити, оскільки замикання, що передається в setTimeout, продовжує існувати. У свою чергу, в ньому зберігаються посилання на myObj допомогою myRef. Це те ж саме, як якщо б ми передали замикання в будь-яку іншу функцію, залишивши посилання на нього.

Потрібно пам'ятати, що посилання всередині викликів setTimeout/setInterval такі, як функції, повинні виконатися і завершитися перед тим, як їх можна буде підчищати.

Бійтеся пасток швидкодії

Важливо не оптимізувати код передчасно. Можна захопитися мікро-тестами, які кажуть, що N швидше M V8, проте реальний внесок цих речей в готовий модуль може бути набагато менше, ніж вам здається.



Скажімо, нам потрібен модуль, що:
— читає з локального джерела дані, що мають чисельні id;
— малює табличку з цими даними;
— додає обробники подій для кліків по клітинках.

Відразу з'являються питання. Як зберігати дані? Як ефективно малювати табличку і вставляти її в DOM? Як обробляти події оптимальним чином?

Перший і наївний підхід — зберігати кожен шматочок даних в об'єкті, який можна згрупувати в масив. Можна використовувати jQuery для обходу даних і малювання таблиці, а потім додати її в DOM. І нарешті, можна використовувати прив'язку подій, щоб додати поведінку по кліку.

От як ви НЕ повинні робити:

var moduleA = function () {

return {

data: dataArrayObject,

init: function () {
this.addTable();
this.addEvents();
},

addTable: function () {

for (var i = 0; i < rows; i++) {
$tr = $('<tr></tr>');
for (var j = 0; j < this.data.length; j++) {
$tr.append('<td>' + this.data[j]['id'] + '</td>');
}
$tr.appendTo($tbody);
}

},
addEvents: function () {
$('table td').on('click', function () {
$(this).toggleClass('active');
});
}

};
}();


Дешево і сердито.

Однак, в даному прикладі ми проходимо тільки по id, по числовим властивостями, які можна було б уявити простіше у вигляді масиву. Крім того, пряме використання DocumentFragment і рідних методів DOM більш оптимально, ніж використання jQuery для створення таблиці, і звичайно, обробляти події через батьківський елемент вийде набагато швидше.

jQuery «за лаштунками» безпосередньо використовує DocumentFragment, але в нашому прикладі код викликає append() в циклі, а кожен з викликів не знає про інші, тому код не може бути оптимізовано. Може, це і не страшно, але краще перевірити тести.

Додавши наступні зміни ми прискоримо роботу скрипта.

var moduleD = function () {

return {

data: dataArray,

init: function () {
this.addTable();
this.addEvents();
},
addTable: function () {
var td, tr;
var frag = document.createDocumentFragment();
var frag2 = document.createDocumentFragment();

for (var i = 0; i < rows; i++) {
tr = document.createElement('tr');
for (var j = 0; j < this.data.length; j++) {
td = document.createElement('td');
td.appendChild(document.createTextNode(this.data[j]));

frag2.appendChild(td);
}
tr.appendChild(frag2);
frag.appendChild(tr);
}
tbody.appendChild(frag);
},
addEvents: function () {
$('table').on('click', 'td', function () {
$(this).toggleClass('active');
});
}

};

}();


Подивимося на інші способи поліпшення швидкодії. Ви могли де-небудь прочитати, що модель прототипів більш оптимальна, ніж модель модулів. Або ж, що фреймворки для роботи з шаблонами сильно оптимізовані. Іноді це дійсно так, але в основному вони корисні, тому що код стає більш зручним для читання. І ще потрібно робити прекомпиляцию. Давайте перевіримо ці твердження:

moduleG = function () {};

moduleG.prototype.data = dataArray;
moduleG.prototype.init = function () {
this.addTable();
this.addEvents();
};
moduleG.prototype.addTable = function () {
var template = _.template($('#template').text());
var html = template({'data' : this.data});
$tbody.append(html);
};
moduleG.prototype.addEvents = function () {
$('table').on('click', 'td', function () {
$(this).toggleClass('active');
});
};

var modG = new moduleG();


Виходить, що в цьому випадку переваги у швидкодії нікчемні. Ці речі використовуються не за швидкодії, а з-за читаності, моделі успадкування та поддерживаемости.

Більш складні проблеми — малювання картинок на полотні і робота з пікселями. Завжди перевіряйте, що саме роблять тести швидкості перед їх використанням. Можливо, що їх перевірки та обмеження будуть настільки штучними, що не згодяться вам у світі реальних додатків. Всю оптимізацію краще тестувати в цілком готовому коді.

Поради щодо оптимізації для V8

Не будемо приводити абсолютно всі поради, а зупинимося на найбільш потрібних.

— деякі моделі заважають оптимізації, наприклад зв'язка try-catch. Подробиці про те, які функції можуть, або не можуть бути оптимізовані, можна отримати з утиліти d8 за допомогою команди --trace-opt file.js
— намагайтеся, щоб ваші функції залишалися мономорфными, тобто щоб змінні (включае властивості, масиви і параметри функцій) завжди містили тільки об'єкти з того ж прихованого класу. Наприклад, не робіть так:

function add(x, y) { 
return x+y;
} 

add(1, 2); 
add('a','b'); 
add(my_custom_object, undefined);


— не завантажуйтесь з непроинициализированных або видалених елементів
— не пишіть величезні функції, т. к. їх складніше оптимізувати

Об'єкти або масиви?
— для зберігання купи чисел або списку однотипних об'єктів використовуйте масив
— якщо семантика вимагає об'єкта з властивостями (різних типів), використовуйте об'єкт. Це досить ефективно з точки зору пам'яті, і досить швидко.
— за елементами з цілочисельними індексами ітерація буде швидше, ніж за властивостями об'єкта
— властивості об'єктів у — штука складна, їх можна створювати через сетери, з різною нумерацією і можливістю запису. Елементи масивів не можна так налаштувати — вони або є, або їх немає. З точки зору движка це допомагає оптимізувати роботу. Особливо, якщо масив містить числа. Наприклад, при роботі з векторами використовувати масив замість об'єкта з властивостями x,y,z.

Між масивами та об'єктами JS є одне серйозне відмінність — властивість length. Якщо ви відстежуєте цей параметр, то об'єкти будуть приблизно такими ж швидкими, як і масиви.

Поради щодо використання об'єктів
Створюйте об'єкти через конструктор. Тоді у всіх об'єктів буде один прихований клас. Крім того, це трохи швидше, ніж Object.create().

На число різних типів об'єктів і їх складності обмежень немає (в розумних межах — довгі ланцюжки прототипів шкідливі, а об'єкти з невеликою кількістю властивостей представляються движком дещо по-іншому і трохи швидше, ніж великі). Для «гарячих» об'єктів намагайтеся робити короткі ланцюжки наслідувань і невелике число властивостей.

Клонування об'єктів
Часта проблема. Будьте обережні з копіюванням великих речей — зазвичай це відбувається повільно. Особливо погано використовувати для цього цикли for..in, які повільно працюють в будь-яких движках.

Коли ну дуже треба швидко скопіювати об'єкт, використовувати масив або спеціальну функцію, яка копіює безпосередньо кожне властивість. Ось так буде швидше за все:

function clone(original){
this.foo = original.foo;
this.bar = original.bar;
}
var copy = new clone(original);


Кешування функцій Модульної моделі
Ця техніка може поліпшити швидкодію. Ті варіанти прикладу нижче, які ви напевно зустрічали, швидше за все, працюють повільніше, оскільки вони весь час створюють функції-члени.



Ось тест на швидкодію прототипів супроти модулів:

// Модель прототипів
Klass1 = function () {}
Klass1.prototype.foo = function () {
log('foo');
}
Klass1.prototype.bar = function () {
log('bar');
}

// Модель модулів
Klass2 = function () {
var foo = function () {
log('foo');
},
bar = function () {
log('bar');
};

return {
foo: foo
bar: bar
}
}


// Модулі з кешування функцій
var FooFunction = function () {
log('foo');
};
var BarFunction = function () {
log('bar');
};

Klass3 = function () {
return {
foo: FooFunction,
bar: BarFunction
}
}

// Ітераційні тести

// Прототипи
var i = 1000,
objs = [];
while (i--) {
var o = new Klass1()
objs.push(new Klass1());
o.bar;
o.foo;
}

// Модулі
var i = 1000,
objs = [];
while (i--) {
var o = Klass2()
objs.push(Klass2());
o.bar;
o.foo;
}

// Модулі з кешування функцій
var i = 1000,
objs = [];
while (i--) {
var o = Klass3()
objs.push(Klass3());
o.bar;
o.foo;
}
// Звертайтеся до тесту за подробицями


Якщо вам не потрібен клас, не створюйте його. Ось приклад того, як можна поліпшити швидкодію, позбувшись від накладок, пов'язаних з класами jsperf.com/prototypal-performance/54.

Поради щодо використання масивів
Не видаляйте елементи. Якщо у масиві утворюються порожні місця, V8 переключається на словниковий метод роботи з масивами, що робить скрипт ще повільніше.

Літерали масивів
Корисні, т. к. натякають V8 щодо типів та кількості елементів в масиві. Підходять для невеликих і середніх масивів.

// V8 знає, що вам потрібен масив чисел з 4 елементів:
var a= [1, 2, 3, 4];

// Не треба так:
a = []; // V8 нічого не знає про масив - зовсім як Джон Сноу
for(var i = 1; i < = 4; i++) {
a.push(i);
}


Одинарні або змішані типи
Не змішуйте різні типи в одному масиві (var arr = [1, «1», undefined, true, «true»])

Тестування швидкодії змішаних типів

З тесту видно, що швидше усього працює масив цілих чисел.

Розріджені масиви
В таких масивах доступ до елементів працює повільніше — V8 не займає пам'ять для всіх елементів, якщо використовуються тільки декілька. Вона працює з ним за допомогою словників, що економить пам'ять, але позначається на швидкості.

Тестування розріджених масивів

«Діряві» масиви
Уникайте дірявих масивів, що виходять при видаленні елементів, або присвоєння a[x] = foo, де x > a.length). Якщо видалити лише один елемент, робота з масивом сповільнюється.

Тест дірявих масивів

Попереднє заповнення масивів або заповнення на льоту
Не варто попередньо заповнювати великі масиви (більше 64К елементів). Nitro (Safari) працює з попередньо заповненими масивами краще. Але інші движки (V8, SpiderMonkey) працюють інакше.



Тест предзаполненных масивів

// Порожній масив
var arr = [];
for (var i = 0; i < 1000000; i++) {
arr[i] = i;
}

// Предзаполненный масив
var arr = new Array(1000000);
for (var i = 0; i < 1000000; i++) {
arr[i] = i;
}


Оптимізація програми

Для веб-додатків швидкість — це головне. Користувачі не люблять чекати, тому критично намагатися вичавити всю можливу швидкість з скрипта. Це досить важке завдання, і ось наші рекомендації по її виконанню:



— виміряти (знайти вузькі місця)
— зрозуміти (знайти, у чому проблема)
— пробачити виправити

Тести швидкості (бенчмарки)
Звичайний принцип вимірювання швидкості — заміряти час виконання і порівняти. Одна модель порівняння була запропонована командою jsPerf і використовується в SunSpider і Kraken:

var totalTime,
start = new Date,
iterations = 1000;
while (iterations--) {
// Тут йде досліджуваний код
}
// totalTime → кількість мілісекунд, 
// необхідні для виконання коду 1000 разів
totalTime = new Date - start;


Код поміщається в цикл і виконується кілька разів, потім з часу закінчення віднімається час початку.

Але це занадто простий підхід — особливо для перевірки роботи в різних браузерах або оточеннях. На швидкодію може впливати навіть збірка сміття. Про це потрібно пам'ятати навіть при використанні window.performance

Для серйозного занурення в тестування коду рекомендую прочитати JavaScript Benchmarking.

Профілювання
Chrome Developer Tools підтримують профілювання. Його можна використовувати, щоб дізнатися, які функції отжирают найбільше часу, і оптимізувати їх.



Профілювання починається з визначення точки відліку для швидкодії вашого коду — для цього використовується Timeline. Там зазначено, як довго виконувався наш код. В закладці «профілі» більш докладно зазначено, що відбувається в додатку. Профіль JavaScript CPU показує, скільки процесорного часу забрав код, CSS selector — скільки часу пішло на обробку селекторів, а Heap snapshots показує використання пам'яті.

За допомогою цих інструментів можна ізолювати, підправити і перепрофілювати код, вимірюючи, як при цьому змінюється виконання програми.



Хороші інструкції щодо профілізації знаходяться тут: JavaScript Profiling With The Chrome Developer Tools.

В ідеалі, на профілювання не повинні впливати встановлені розширення і програми, тому запускайте Chrome з параметром the --user-data-dir <пустая_директория>.

Уникаємо витоків пам'яті — техніка трьох знімків пам'яті
У Google Chrome Developer Tools активно використовуються в проектах на зразок Gmail для виявлення та усунення витоків.



Деякі параметри, на які наші команди звертають увагу — приватне використання пам'яті, розмір купи JS, кількість вузлів DOM, чистка сховища, лічильник обробників подій, збірка сміття. Знайомим з подієвими архітектурами буде цікаво, що найбільш часті проблеми у нас виникали, коли у listen() відсутній unlisten() (замикання) і коли немає dispose() для об'єктів, що створюють обробники подій.

Є чудова презентація техніки «3 знімків», яка допомагає знаходити витоку через DevTools.

Сенс техніки в тому, що ви записуєте кілька дій у вашому додатку, запускаєте збірку сміття, перевіряєте, чи повертається кількість вузлів DOM до очікуваного значення, і потім аналізуєте три знімки купи для визначення наявності витоків.

Керування пам'яттю в односторінкових додатках
В сучасних односторінкових додатках важливо керувати пам'яттю (фреймворки AngularJS, Backbone, Ember), тому що вони не перезавантажуються. Тому витоку пам'яті можуть швидко проявити себе. Це велика пастка для таких додатків, тому що пам'ять обмежена, а додатки працюють тривалий час (емейл-клієнти, соц.мережі). Велика влада — велика відповідальність.

У Backbone переконайтеся, що ви позбавляєтеся від старих видів і посилань через dispose(). Ця функція була додана нещодавно, вона видаляє всі хендлеры, додані об'єкт events, і всі колекції обробників, коли вид передається як третій аргумент (у зворотних дзвінків). dispose() викликається в функції view remove(), що вирішує більшість простих проблем з очищенням пам'яті. У Ember подчищайте оглядачів, коли вони виявляють, що елемент був видалений з виду.

Порада від Деріка Бейлі::

Розберіться, як, з точки зору посилань працюють події, а в іншому дотримуйтесь стандартним правилам по роботі з пам'яттю, і все буде ОК. Якщо ви завантажуєте дані в колекцію Backbone, в якій багато об'єктів User, ця колекція повинна бути підчищена, щоб вона не використовувала більше пам'яті, вам потрібно видалити всі посилання на неї і всі об'єкти окремо. Коли ви видалите всі посилання, все буде очищено.


цієї статті Деррік описує безліч помилок по роботі з пам'яттю при роботі з Backbone.js, а також пропонує вирішення цих проблем.

Ще один хороший тьюториал по налагодженню витоків в Node.

Мінімізуємо перерахунок позицій і розмірів елементів при оновленні зовнішнього вигляду сторінки
Такі перерахунки блокують сторінку для користувача, тому потрібно розібратися в тому, як зменшити час перерахунку. Методи, що викликають перерахунок, треба зібрати в одному місці та використовувати їх рідко. Потрібно проводити якомога менше дій безпосередньо з DOM. Для цього служить DocumentFragment — спосіб виокремити частину дерева документа. Замість постійного додавання вузлів DOM, ми можемо використовувати фрагменти для побудови всього необхідного, а потім виконати одну вставку в DOM.



Зробимо функцію, добавляющую 20 div елемент. Просте додавання кожного div викличе 20 перерахунків сторінки.

function addDivs(element) {
var div;
for (var i = 0; i < 20; i ++) {
div = document.createElement('div');
div.innerHTML = 'Heya!';
element.appendChild(div);
}
}


Замість цього можна використовувати DocumentFragment, додати div до нього, а потім додати його в DOM через appendChild. Тоді всі спадкоємці фрагмента будуть додані до сторінки за один перерахунок.

function addDivs(element) {
var div; 
// Creates a new empty DocumentFragment.
var fragment = document.createDocumentFragment();
for (var i = 0; i < 20; i ++) {
div = document.createElement('a');
div.innerHTML = 'Heya!';
fragment.appendChild(div);
}
element.appendChild(fragment);
}


Докладніше — у статтях Make the Web Faster, JavaScript Memory Optimization і Finding Memory Leaks.

Детектор витоку пам'яті JavaScript
Щоб допомогти з виявленням витоків, була розроблена утиліта для Chrome Developer Tools, що працює через протокол віддаленої роботи, яка робить знімки купи і з'ясовує, які об'єкти служать причиною витоку.



Рекомендую ознайомитися з постом на цю тему або почитати сторінку проекту.

Прапори V8 для налагодження та оптимізації збірки сміття
Відстеження оптимізації:

chrome.exe --js-flags="--trace-opt --trace-deopt"


Детальніше:

trace-opt — записувати імена оптимізованих функцій і показувати пропущений код, за яким оптимізатор не впорався
trace-deopt — записувати код, який довелося деоптимизировать при виконанні
trace-gc — записувати кожен етап збирання сміття

Оптимізовані функції позначаються зірочкою (*), а не оптимізовані — тільда (~).

Пікантні подробиці про прапори і внутрішній роботі V8 читайте в пості В'ячеслава Єгорова.

Час високого дозволу і Navigation Timing API
Час високого дозволу (High Resolution Time, HRT) — це інтерфейс JS для доступу до таймеру з роздільною здатністю менше мілісекунди, який не залежить від зміни часу користувачем. Корисний для написання тестів швидкодії.

Доступний в Chrome (stable) як window.performance.webkitNow(), а в Chrome Canary без префікса-window.performance.now(). Підлогу Айріш написав про це докладно у своєму пості на HTML5Rocks.

Якщо нам необхідно виміряти роботу програми в інтернеті, нам допоможе Navigation Timing API. З його допомогою можна одержати точні і детальні вимірювання, що виконуються при завантаженні сторінки. Доступно через window.performance.timing, яку можна використовувати прямо в консолі:



З цих даних можна дізнатися багато корисного. Наприклад, затримка мережі responseEnd-fetchStart; час, який потрібно витратити на завантаження сторінки після отримання з сервера loadEventEnd-responseEnd; час між завантаженням сторінки і стартом навігації loadEventEnd-navigationStart.

Подробиці можна дізнатися у статті Measuring Page Load Speed With Navigation Timing.

about:memory about:tracing
about:tracing в Chrome показує інтимні подробиці про швидкодію браузера, записуючи всю його діяльність в кожному з тредів, закладок і процесів.



Тут можна побачити всі подробиці, необхідні для профілювання скрипта і підправити розширення JS таким чином, щоб оптимізувати завантаження.

Хороша стаття про використання about:tracing для профілювання WebGL-ігор.

about:memory Chrome — також корисна штука, яке показує, скільки пам'яті використовує кожна закладка — це можна використовувати для пошуку витоків.

Висновок

У дивному і загадковому світі движків JS є багато підводних каменів, пов'язаних з швидкодією. Не існує універсального рецепту для покращення швидкодії. Комбінуючи різні техніки оптимізації і тестуючи програми в реальному оточенні можна побачити, яким чином потрібно оптимізувати ваш додаток. Розуміння того, як движки обробляють і оптимізують ваш код, може допомогти вам у налаштуванні програм. Вимірюйте, розумійте, виправляйте і повторюйте.



Не забувайте про оптимізацію, але не займайтеся мікро-оптимізацією за рахунок зручності. Думайте, яка оптимізація важлива для програми, а без якої воно не може обійтися.

Майте на увазі, що оскільки движки JS стають все швидше, таким вузьким місцем виявляється DOM. Перерахунок і перемальовування теж необхідно мінімізувати чіпайте DOM тільки у випадку абсолютної необхідності. Не забувайте про мережу. HTTP-запити також потрібно мінімізувати і кешувати, особливо у мобільних додатків.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.