JavaScript. Пишем быстрый, экономящий память код.
4 февраля в 12:54 [перевод] [ javascript] [ быстродействие] [ V8]
Движки на JavaScript, такие как V8 от Google специально созданы для повышения быстродействия больших приложений написанных на JavaScript. Если вы разрабатываете на JavaScript, и вы заботитесь о потреблении памяти и быстродействии скрипта, вы должны точно знать, что происходит под капотом браузера и как этот движок крутит свои шестеренки. Будь это V8, SpiderMonkey(Firefox), Caracan(Opera), Chakra (IE) или какой либо другой. Изучая их, вы помогаете себе оптимизировать ваши приложения. Это не значит, что нужно оптимизировать для определённого движка браузера.
Стоит задать себе вопросы:
- Могу ли я как-либо оптимизировать мой код?
- Какие (обычные) оптимизации делают популярные JavaScript движки?
- Для чего не сможем оптимизировать движок, и собирает ли «сборщик мусора», то, что он должен собирать?
Есть много распространенных ошибок при написании приложений эффективно использующих виртуальную память. Со временем выполнения скриптов также встречаются распространенные ошибки. В этой статьи мы рассмотрим подходы написания кода, ускоряющих наш код, также будут представлены результаты тестов по представленным рецептам.
Так как же все-таки работает JavaScript в движке V8?
Писать масштабные JavaScriptприложения можно, и не заглядывая под капот браузера, но каждый водитель хотя-бы раз обозревал «движущее ядро» своего автомобиля. Так моим основным браузером является Chrome, я расскажу немного о его JavaScriptдвигателе. V8 состоит из нескольких основных компонентов.
- Базовый компилятор, который парсит ваш JavaScript код и генерирует родной машинно-понятный код, до того, как он исполняется, вместо того, чтобы исполнять его или просто интерпретировать. Этот код изначально, не очень хорошо оптимизировать.
- V8 представляет ваши объекты в виде объектной модели. Объекты в JavaScript представлены как ассоциативные массивы, но в V8 они представлены в виде скрытых классов, которые являются внутренний тип, для оптимального внутрисистемного поиска.
- Профайлер исполнения кода следит за системой и определяет «горячий» код (т.е. тот который занимает много времени на выполнение).
- Оптимизирующий компилятор перекомпилирует и оптимизирует «горячий» код который обнаружил профайлер, например встраивание (т.е. вставка вместо вызова функции, тело самой функции).
- V8 поддерживает «переоптимизацию». Это значит, что движок после проделанной оптимизации решил, что его выводы были слишком оптимистичны относительно кода, и он может «помочь» ему ещё раз.
- У движка есть «сборщик мусора». Понимание того как он работает, может быть также важно как и оптимизированный код.
«Сборщик мусора»
Собирание мусора это своего рода форма управления памятью. Коллектор, который пытается забрать ту память занятую вашими объектами, и которая больше не используется. В «сборщике мусора» языка JavaScript, объекты имеющие ссылку на переменную в вашем коде, не зачищаются. В большинстве случаев убирать ссылки вручную не требуется. Следует лишь держать объявленные переменные в своей области видимости, а не в глобальной.
В языке JavaScript нет возможности управления «сборщиком мусора». Думаю, вы не захотели им управлять, так как процесс происходит на лету, тем более, что он лучше знает как всё подчищать.
Зачищаем заблуждения
В нескольких онлайновых обсуждениях об очистке памяти в JavaScript было выставлено на обсуждение ключевое слово delete
. Судя по названию, оно должно удалять элемент, и многие разработчики думают, что удаляется и ссылка. Избегайте использования delete
если можно. В примере ниже , delete o.x делает намного больше вреда, чем пользы, так как под капотом это делает скрытый класс, обычным медленным объектом.
var o = { x:1 }; delete o.x; // true o.x; // undefined
В любой популярной JavaScript библиотеке наверняка есть отсылка на delete
– у него есть применение в языке. Основной посыл здесь – избегать изменения «горячих объектов» на лету. JavaScript движки способны определять такие «горячие объекты», оптимизируя их в процессе. Это проще если архитектура объекта значительно не меняется в течении цикла его жизни, и delete
может активировать подобные изменения, тем самым наделав бед.
Также существует заблуждение насчет того, как работает null
. Назначая null
переменной, это не «обнуляет» объект, теперь ссылка в памяти ведет на null
. Использование o.x = null
лучше, чем delete
, но это по большому счету не обязательно.
var o = { x: 1 }; o = null; o; // null o.x // TypeError
Если ссылка на объект была последней, объект становится кандидатом на сбор мусора. И само собой, если есть ссылка на объект, сборки мусора не произойдет. Другая важная деталь, это то, что глобальные переменные не зачищаются в течении того времени пока ваша страница открыта.
var myGlobalNamespace = {};
Глобальные переменные подчищают: когда вы обновляете страницу, переходите на другую страницу, закрываете вкладку или закрываете браузер. Переменные, объявленные в функции, подчищаются сразу, как функция отработала свою задачу.
Правила на каждый день
Чтобы дать сборщику мусора собрать как можно больше объектов, как можно раньше, не храните объекты, которые вы не используете. В большинстве случаев это происходит автоматически, вот несколько вещей, которые стоит иметь в виду.
- Как было упомянуто ранее, лучшая альтернатива ручной «разлинковке», это использование переменных внутри функций, вместо глобальных. Это значит, что будет меньше вещей, о которых надо будет беспокоиться.
- Важно удалять обработчики событий. Особенно с элементов DOM которые вскоре должны быть удалены.
- Если вы используете локальный кэш, стоит позаботиться о его удалении, или реализовать механизм удаления, что позволит избавится от больших кусков кода, которые вы, скорее всего, не будете использовать.
Функции
Следующим разберем функции. Как мы уже уяснили, «сборщик мусора» возвращает себе неиспользуемую память отработавших объектов. Чтобы как-то это показать, вот несколько примеров.
function foo() { var bar = new LargeObject(); bar.someCall(); }
Когда fooвозвращается, объект, на который указывает barавтоматический кандидат на удаление из памяти, потому как не осталось указывающих на объект ссылок.
Сравните это с:
function foo() { var bar = new LargeObject(); bar.someCall(); return bar; } // где то в другом месте var b = foo();
У нас теперь есть выживший 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
В примере функция sum не может быть собрана «сборщиком мусора», так как объект-функция присвоена к глобальной переменной, и до сих пор доступна. Функцию sum можно использовать через sumA(n)
.
Давайте взглянем на другой пример. Вот, имеем ли мы доступ к largeStr
?
var a = function () { var largeStr = new Array(1000000).join('x'); return function () { return largeStr; }; }();
Да имеем через a()
, значит сборщик её оставил. Как вам такое?
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('Time is running out!'); 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, intзначение которое можно представить в виде массива. Можно использовать DocumentFragment
и JavaScript методы для управления DOM. Это будет оптимально, чем использование JQuery (в такой манере) для генерации таблицы, и конечно, делегирование событий обычно лучше, чем прикрепление каждого tdиндивидуально.
Нужно иметь ввиду, что JQueryсам использует DocumentFragment
, но в нашем примере мы используем append()
внутри цикла, и каждый из этих вызовов мало знает друг о друге поэтому трудно оптимизировать данный кусок кода. В этом случае это не должно стать проблемой, но стоит тестировать свой код на постоянной основе. В нашем случае применение этих мер повысило производительность. Делегирование событий оказалось эффективнее, чем просто прикрепление (binding
), и переход к documentFragment был реальным скачком в производительности.
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'); }); } }; }();
Будем искать дальнейшие пути оптимизации. Вы возможно где-то читали, что использование прототипный шаблон оптимально, чем модульный шаблон (мы подтвердили ранее, что это не так), или слышали, что JavaScriptшаблонизаторы высоко оптимизированы. Иногда они эффективны, но использовать их только ради хорошо читаемого кода. Также, прекомпиляция! Давайте посмотрим на практике, насколько это обоснованно.
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();
Выходит так, что выигрыш в производительности минимален.
Настоящая причина почему разработчики используют эти инструменты — читаемость кода, наследование, удобство поддержки, привносимое в ваш код. Есть проблемы и поинтереснее, эффективность отрисовки изображений с помощью canvas и манипуляция данными о пикселяхс или без типизированных массивов.
Всегда уделяйте микро тестам должное внимание, прежде чем использовать в вашем приложении. Многие кто читал, могут вспомнить «перестрелку JavaScript шаблонизаторов» и последовавшую «расширенную перестрелку». Нужно убедиться в том, что результаты тестов не столкнутся с ограничениями в реальных приложениях — тестируйте оптимизации в реальном коде.
V8 Заметки по оптимизации
Разбирать каждую оптимизацию V8 выходит за пределы этой статьи, и также есть вовсе бесполезные рекомендации. Имея это ввиду, поможет вам писать более эффективный код.
- Некоторые шаблоны помогут в оптимизации. Блок
try-catch
, например, поможет. Чтобы узнать какие функции можно оптимизировать, а какие нет, можно использовать–trace-optfile.js
с d8 утилитой, которая поставляется с V8.
- Если вы заботитесь о скорости выполнения, старайтесь чтобы ваши функции оставались мономорфными. Не стоит пихать в них разные типы данных.
function add(x, y) { return x+y; }
add(1, 2); add('a','b'); add(my_custom_object, undefined);
- Не стартуйте с не инициализированных или удаленных объектов. Это конечно не сделает погоды на выходе, но замедлит общую картину.
- Не раздувайте функции до огромных размеров, их сложнее оптимизировать.
Больше рецептов можно почерпнуть здесь: За пределы скоростных ограничений с V8.
Также стоит почитать: Оптимизируем для V8 — серия статей.
Объекты против массивов. что использовать?
- Если хотите использовать набор чисел, или список объектов одного типа, используйте массив.
- Если семантически вам нужен объект с его свойствами (разных типов), используйте его. Это экономит память, и быстродействие не хромает.
- Через контейнер с нумерованным индексом итерация проходит намного быстрее, чем через объект с свойствами.
- Свойства объектов довольно универсальны: они могут быть с сеттерами, и с нумерацией в любом порядке и свойством менять состояние. Составляющие в массиве не могут так легко менять свое состояние — они либо есть, либо нет. На уровне движка это оставляет маневр для оптимизации памяти, представляющей структуру. Это особенно выгодно, когда массив содержит числа. Например, вам нужен векторы, не объявляйте класс с множеством аргументов x, y, z; используйте массив.
Есть одно основное различие между объектами и массивами в JavaScript, это магическое свойство length
. Если реализовывать это свойство самостоятельно, объекты в V8, по скорости, могут равняться с массивами.
Когда же использовать объекты?
- Создание класса с помощью функции конструктора, даёт гарантию, что все объекты, созданные им будут иметь один и тот-же скрытый класс, и помогает избежать конфликтов. Как финиш, способ намного быстрее, чем Object.create().
- Нет каких-либо ограничений на количество различных типов объектов, или их сложность. Но есть факты: длинные цепочки прототипов будут вредить, и объекты с несколькими свойствами получают особую реализацию, и это немного быстрее, чем большие объекты. Для «горячих» объектов, старайтесь держать цепочки прототипа короткими, а колличество свойств минимальным.
Клонирование объектов
Клонирование объектов является рядовой задачей для разработчиков. Хотя и возможно протестировать разные реализации решающие эту проблему в V8, нужно быть осторожнее с клонированием. Клонирование – медленная операция – не надо. for..in
циклы в JavaScript имеют запутанную спецификацию и похоже, что они никогда не подойдут для этой задачи, в том числе для случайных объектов.
Когда вам точно понадобится клонировать объект, в критически важной области кода, с точки зрения производительности (и сроки горят), используйте массив или свой "copy конструктор
“ которая явно скопирует каждое свойство объекта. Это, пожалуй, самый быстрый способ:
function clone(original) { this.foo = original.foo; this.bar = original.bar; } var copy = new clone(original);
Кэширование функций в шаблоне «Модуль»
Кэширование ваших функций, используя шаблон «Модуль», может повысить общую производительность. Внизу примеры, которые вы возможно использовали, и они медленнее, т.к. они принуждают копирование функций во время выполнения.
Вот тест производительности шаблонов «прототип» и «модуль»
// Prototypal pattern Klass1 = function () {} Klass1.prototype.foo = function () { log('foo'); } Klass1.prototype.bar = function () { log('bar'); } // Module pattern Klass2 = function () { var foo = function () { log('foo'); }, bar = function () { log('bar'); }; return { foo: foo, bar: bar } }Замечание: Если класс не требуется, не стоит его создавать. Вот пример теста где была убрана необходимость создания класса. http://jsperf.com/prototypal-performance/54 .
// Шаблон "модуль" с кэшированием функций var FooFunction = function () { log('foo'); };
var BarFunction = function () { log('bar'); };
Klass3 = function () { return { foo: FooFunction, bar: BarFunction } } // Теcты с циклами // Шаблон "прототип" 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; } // Посмотрите результаты тестов
Заметки: когда использовать массивы
Следующим разберем использование массивов. Всё просто, не удаляйте элементы массива. Это сделает внутреннюю обработку массива очень медленной. Когда в массиве появляются пробелы, V8 переключить обработку в режим словаря, что ещё медленее.
Элементы массива
Объявленные элементы массива полезны, т.к. V8 понимает, что представляет из себя массив. Хорошо для малых и средних размеров массива.
// Здесь V8 все четко видит 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
). Даже один удаленный элемент сильно замедлит приложение.
Тест «цельного» и «дырявого» массивов.
Определённый размер массива и неопределённый
Лучше оставлять массив как есть, и не предопределять его размер.
Nitro (Safari) есть выигрыш в скорости с предопределённым массивом. Но в других движках (V8, SpiderMonkey), неопределённый размер массива выигрывает.
Тест предопределённых массивов:
// Empty array var arr = [];
for (var i = 0; i < 1000000; i++) { arr[i] = i; }
// Pre-allocated array var arr = new Array(1000000); for (var i = 0; i < 1000000; i++) { arr[i] = i; }
Оптимизируйте ваш код
В мире веб-приложений, скорость критична. Ни один пользователь не будет ждать минуту, пока его сообщение опубликовывается. Вот почему нужно выгадывать каждую миллисекунду.
В процессе оптимизации своего приложения следует держать несколько пунктов в голове:
- Измеряйте производительность: Локализуйте медленные места (~45%)
- Поймите что не так: Выделите проблему (~45%)
- Исправьте! (~10%)
Далее будет рассмотрено несколько инструментов для тестирования.
Тестирование производительности
Есть множество инструментов для тестирования производительности JavaScript — обычное мнение, что тесты просто сравнивают две временные линии. Такой способ тестирования использует команда jsPerf, и подобное используют SunSpiders’s и Kraken’s тестировщики производительности.
var totalTime, start = new Date, iterations = 1000;
while (iterations--) { // Code snippet goes here }
// totalTime → the number of milliseconds taken // to execute the code snippet 1000 times totalTime = new Date - start;
Код при начале теста помещается в цикл, и прокручивается несколько раз (здесь 6). Затем время начала вычитается из времени конца, получаем результат.
Это конечно упрощенная модель теста, на самом деле все сложнее, не говоря уже о тестировании разных движков браузеров. Один только «сборщик мусора» может повлиять на результат теста производительности. Даже если вы используете window.performance, вам все равно нужно все учесть. Независимо от того, тестируете ли вы кусок кода, пишите библиотеку для тестирования, дальше больше, всегда найдется JavaScriptкод который надо протестировать. Для более подробного изучения вопроса тестирования производительности, я очень рекомендую прочитать статью JavaScriptBenchmarkingавторов MathiasBynens и John-DavidDalton.
Профилирование
У инструментов Chrome есть отличная поддержка JavaScript профилирования. Их можно использовать для обнаружения функций, которые съедают много времени, которыми можно потом заняться. Это очень важно, одно малое изменение может иметь критические последствия на общую производительность.
Профилирование начинается с точки производительности вашей системы, которую можно вычислить, используя временную шкалу(Timeline). Это покажет нам, сколько наш код выполнялся. Вкладка Profiles дает нам лучшее представление о том, что происходит внутри нашего приложения. Вкладка CPU поможет увидеть процент использования процессора. CSS вкладка, покажет, сколько селекторы выполняли свою работу. И снимок «кучи» покажет, сколько памяти все это заняло. Использование этих инструментов, мы можем изолировать, и подправить и перепрофилировать, в итоге измерить те изменения, которые мы провели в функциях или операциях.
Для глубокого погружения в профилирование, прочитайте JavaScript Profiling With The Chrome Developer Tools, автор ZackGrossbart.
Замечание: В идеале мы должны быть уверены, что никакие ваши расширения браузера не влияют на результат, поэтому запускаем Chromeс флагом --user-data-dir <empty_directory>
.
В большинстве случаев такого подхода к профилированию будет достаточно, если понадобится больше, нужно использовать флаги V8.
Избегаем утечки памяти — техника 3-х снимков
В компании Google, команды, работающие с Gmail, например, часто используют инструменты разработчика, мы также используем их для отслеживания утечек памяти.
Статистика, которая интересна нашей команде – это использование памяти, и размер «кучи». Кол-во DOM элементов, очистка хранилища, обработчики событий, и то, что происходит со «сборщиком мусора». Для тех, кто знаком с архитектурой, основанной на событиях, вам может быть интересно, то, что у нас были проблемы с listen()
без unlisten()
и отсутствующие dispose()
для объектов создающих обработчики событий.
К счастью инструменты разработчики помогут решить нам некоторые проблемы, и Loreena Lee презентовала фантастический документ “3 snapshot” technique которую я не могу не порекомендовать. Суть этой техники состоит в записи определённых действий в вашем приложении, принуждаете работать «сборщика мусора», проверяете, не вернулось ли кол-во элементов DOM к вашей линии ожидаемого кол-ва элементов, и в конце смотрите на 3 снимка «кучи» для определения, нет ли утечки.
Управление памятью в одностраничном приложении
Управление памятью очень важно, если вы пишите одностраничные приложения (т.е. AngularJS, Backbone, Ember) так как они почти никогда не перезагружаются. Это означает, что, утечка памяти становится очевидной очень быстро. Это большая проблема в мобильных одностраничных приложениях, память там очень ограничена, и для «долгоиграющих» приложений наподобие почтовых клиентов, или социальных, этот вопрос критичный. С большой силой приходит большая ответственность.Есть несколько способов предотвращения вышеупомянутых проблем. В основе, убедитесь, что вы удаляете «виды» и ссылки используя dispose()
(доступно в Backbone (edge)). Эта функция была добавлена недавно, она удаляет все обработчики, добавленные в «виды» «события» объекта, как и любую коллекцию, в которую «вид» передается в качестве аргумента (в контексте функции обратного вызова). dispose()
вызывается методом «вида» remove(), тем самым заботясь о очистке памяти, во время очистки экрана.
Другие библиотеки, такие как Ember подчищает самих наблюдателей, когда они видят, что элемент был удалён из «вида» дабы избежать утечек памяти.
Мудрый совет от Derick Bailey:
«Чем разбираться в том, как работает система ссылок, просто следуйте правилу управления памятью, и всё будет ок. Если вы загружаете данные в основу вашего приложения, полную пользовательских объектов, после отработки нужно убрать все ссылки на коллекцию и отдельные объекты в ней. Как только вы уберете все ссылки, всё будет чисто. Это очень похоже на поведение "сборщика мусора".»
В этой статье, Derich обсуждает множество частых ошибок, связанных с памятью, при работе с backbone.js
их как их исправлять. Стоит прочитать статью по дебагу о утечках в памяти в Node, автор Felix Geisendörfer.
Минимизируем перекомпановки (reflow)
Когда браузер пересчитывает позиции элементов и геометрию в документе, с целью перерисовки, мы называем это reflow. Reflow операция, блокирующая действия пользователя, поэтому полезно знать, как сократить это время. Нужен контроль над методами, которые осуществляют перерисовку и reflow, и использовать их в тандеме. Нужно обрабатывать DOM там, где это возможно. Используйте DocumentFragment, лёгкий объект-документ. Представьте это как изъятие куска дерева документа, или создание нового «фрагмента» документа. Это будет лучше, чем постоянно добавлять «ноды» в документ, мы можем использовать фрагменты документов, проделать все нужные операции, и вставить в DOM, тем самым избавившись от ненужных reflow. Для примера напишем функцию, добавляющую 20 div-ов к элементу. Просто прикрепив эти новые div может запустить 20 reflows.
function addDivs(element) { var div; for (var i = 0; i < 20; i ++) { div = document.createElement('div'); div.innerHTML = 'Heya!'; element.appendChild(div); } }
Для того чтобы обойти этот момент, мы используем DocumentFragment
присоединяем все новые div
к this
. Использование DocumentFragment
с методом appendChild
провоцирует лишь один reflow.
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); }
По этой теме можно почитать Делаем Веб Быстрее (англ.), Оптимизация памяти JavaScript (англ.) и Ищем утечки памяти (англ.).
Детектор утечек памяти javascript
Для обнаружения утечек памяти, два моих друга (Marja Hölttä и Jochen Eisinger) разработали инструмент, работающий совместно с Chrome инструментами разработчика (в частности, протокол удалённого анализа), также получающий снимок «кучи» и обнаруживающий какие объекты создают утечки.
Пост о том, как пользоваться инструментом, советую также ознакомиться с страницей Страница – Детектор Утечек Памяти.
Немного информации: Для тех, кто недоумевает, почему этот инструмент не был включен в инструменты разработчика Chrome, причина такова. Инструмент был разработан для обнаружения специального рода сценариев утечки памяти в библиотеке Closure, и лучше оно будет в виде расширения (когда мы реализуем API профилирования «кучи»).
V8 Флаги для дебага, оптимизации и сборщика мусора
Chrome поддерживает передачу флагов прямо в V8 черезjs-flags
для получения подробной информации, какие оптимизации проводит движок. Например, это отслеживает оптимизации V8:
"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"
Для пользователей Windows chrome.exe --js-flags="--trace-opt --trace-deopt"
При разработке ваших приложений, можно использовать эти флаги V8.
trace-opt
– Ведет лог оптимизируемых функций и показывает, какие куски кода пропускает, т.к. не разобрался в нем.trace-deopt
– выводит список кода, который ему приходится деоптимизировать.trace-gc
– логирует каждое действие сборщика мусора.
V8 помечает оптимизированные функции *
(звездочкой) и неоптимизированные ~
(тильда).
Если вам интересно узнать больше о флагах V8, и как шестерёнки крутятся, есть отличная статья Вячеслава Егорова, сборник всех ресурсов по этому вопросу, лучший на данный момент.
Временная шкала высокого разрешения
High Resolution Time (HRT) JavaScript интерфейс предоставляющий текущее время в микросекундах, не зависящее от системных часов или изменений пользователя. Думайте об этом как о более точной версии new Date
и Date.now()
. Это полезно для тестов производительности. HRT доступен в Chrome (stable) как window.performance.webkitNow()
, префикс был отброшен в версии Canary, теперь команда вида window.performance.now()
. Paul Irish опубликовал статью о HRT на ресурсе HTML5Rocks. У нас есть текущее время, что если нам нужен API для более точного измерения производительности?
Ещё много информации доступно у Navigation Timing API. Этот API даёт точный, и детальный анализ времени, которое было записано во время загрузки страницы. Вызвать можно прямо из консоли командой window.performance.timing
.
Много полезной информации даёт данная команда. Например, задержка сети responseEnd-fetchStart
, loadEventEnd-responseEnd
- время потраченное на загрузку страницы, с момента как была получена информация от сервера, loadEventEnd-navigationStart
и время затраченное между навигацией и загрузкой страницы.
Также доступно значение, дающее информацию об использовании памяти, такой как размер «кучи». Более подробно использование Navigation Timing API, расписано в статье Sam Dutton’а Measuring Page Load Speed With Navigation Timing.
О:памяти и о:трассировке
about:tracing в Chrome записывает всё происходящее в браузере, во всех вкладках и процессах.
Самое полезное в этом инструменте, это то, что он позволяет увидеть происходящее во время профилирования, помогает подправлять исполнение вашего JavaScript, или оптимизировать загрузку составляющих приложения. LilliThompsonпишет для разработчиков игр с использованием about:tracingдля профилирования игр на WebGL. Статья будет полезна и рядовым JavaScriptразработчикам.
about:memoryпомогает проследить использование памяти в каждой вкладке, что необходимо в выслеживании потенциальных утечек памяти.
Заключение
Как мы могли убедиться, очень много мудреных вещей происходит в JavaScript движках, и нет универсального решения для увеличения производительности. Только скомбинировав методы (в реальном приложении), можно достичь результатов. Даже после, вас озарит, что знание о том как работает движок того или иного браузера, поможет вам выкрутить производительности приложения на новый уровень. Измеряйте. Понимайте. Исправляйте. Повторить.
Помните об оптимизации, также о том, но излишняя оптимизация не должна вредить удобству. Некоторые разработчики используют .forEach и Object.keys вместо циклов for b for in, даже если они медленнее, нужно помнить, тут важна область видимости. Два слова: здравый смысл.
Движки браузеров становятся быстрее, и следующее узкое место (bottleneck) это DOM. Reflows и перерисовка также важно минимизировать, DOM нужно изменять, только тогда, когда это действительно необходимо. Используйте HTTP кэширование, это особенно критично для мобильных приложений. Взяв всё это на заметку, вы извлечете максимум из этой статьи.
Автор статьи: Addy Osmani
Перевод: Уляшев Роман