Такой движок 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-запросы также нужно минимизировать и кешировать, особенно у мобильных приложений.
Автор: SLY_G