Пишем быстрый и экономный код на JavaScript

http://www.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-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-запросы также нужно минимизировать и кешировать, особенно у мобильных приложений.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 29
    +6
    Если вам кажется необходимым перевести какой-либо материал по программированию, которого ещё не было на Хабре — пишите в личку.
      +5
      Присоединяюсь. Готов перевести интересный материал для Хабра (я переводчик, если что :) JS, веб-программирование, вёрстка — желательно предлагать что-нибудь из этой области.
        +4
        Регулярно в дайджестах есть список статей на английском языке. Можно просто после такого дайджеста устроить голосование и статью-победителя перевести.
          0
          Так же было бы круто перевести статьи по ссылкам из данной
      0
      Ооочень полезная статья. К прочтению.
        +4
        После прочтения ещё будет не лишним поглядеть доклад уже упомянутого в посте Вячеслава Егорова — www.youtube.com/watch?v=65-RbBwZQdU
          +1
          Что-то я сомневаюсь, что картинка со сравнением быстродействия массивов подписана правильно. Скорее всего там всё-таки не число операций в секунду, а время выполнения и, соответственно, чем больше, тем хуже. А значит и вывод сделанный из картинки — не верен.
            0
            это jsperf.com'овые графики, он все выражает в операциях в секунду.
              0
              Я вижу, что это jsperf, поэтому не знаю, почему закралась ошибка, но предпочитаю верить своим глазам и логике. Да и любой желающий может сходить по имеющейся в тексте ссылочке на тот же jsperf и провести тесты самостоятельно, чтобы убедиться в том, что результаты противоположны. Например, у меня для chrome.39 пустой массив выдал 30 op/sec, а предзаполненный 133 op/sec. Для firefox.35 аналогично 81/87, opera.12 — 51/54, safari.5 — 45/46

              Но можно и просто увидеть нелогичность картинки — если ей верить, то у всех браузеров с появлением новых версий производительность падает. Иногда так бывает, но так не может быть для всех браузеров.
                0
                Лимит про 100000 для new Array(n) стал для V8 неактуальным после codereview.chromium.org/397593008, теперь преаллокация быстрее, потому что преалоцированный массив остается быстрым. До этой правки он превращался в словарь.
                  0
                  Между статьёй и вами разница в 15 мажорных версий браузера и более 2 лет времени. Теперь и прототипы быстрее модулей под любым соусом, и предзаполненные массивы на полтора порядка быстрее.
              +1
              «Не удаляйте элементы массивов» — а если это необходимо?
              Нельзя чтоб образовывался 'пробел' в массиве, окей это понятно, но как поступить лучше? Перестроить массив через лодашевский _.without — нормальный вариант?

              «Избегайте использования delete при удалении элементов объекта» — опять же, вот нужно мне по логике удалить элемент из объекта, как лучше поступить?
                +1
                Полагаю, что ничего лучше splice вы не придумаете. _.without создаёт новый массив — а это лишняя память и работа сборщика мусора.
                  0
                  Рекомендации про удаление относится к использованию оператора delete.

                  Если у вас есть объект, который вы используете как словарь (а не как объект), то кроме удаления deleteом ничего не сделаешь.

                  Если же у вас именно объект — то лучше просто сделать o.p = null.

                  С массивом точно также — если массив уже разреженный с кучей дырок, т.е. словарь по сути дела — то пользуйтесь delete, в противном случае либо a[i] = null или двигайте элементы вниз (тем же splice или руками)

                  Все эти рекомендации не следует применять в слепую — надо просто понимать потенциальные проблемы, профилировать и решать уже после профилировки как разбираться с hotspotами.
                    0
                    Небольшой оффтоп, недавно наткнулся на такой странный код:

                    function toFastProperties(obj) {
                      function f() {}
                      f.prototype = obj;
                      return f;
                      eval(obj);
                    }
                    

                    Если не ошибаюсь, этот код — оптимизация для v8, обусловленная разницей внутреннего представления объектов-словарей и объектов-структур. Можете прокомментировать его и актуально ли подобное сейчас?
                      +1
                      Это хак связанный с тем, что V8 старается держать прототипы объектов в fast mode. Получается, когда вы присваиваете медленный объект (dictionary/slow mode) в f.prototype он превращается в быстрый.

                      Сей хак актуален, если вы знаете, что у вас есть объект находящийся по какой-то причине в словарном режиме и вам хочется его превратить в быстрый объект.
                      0
                      a[i] = null в данном случае разве не сделает массив массивом смешанных типов? То есть какого-то однозначно рекомендуемого метода нет, и надо выбирать компромиссное решение в зависимости от ситуации?
                        +1
                        Разумеется если массив у нас изначально состоит из чисел (для других случаев тип элементов не отслеживается), то null действительно переведёт его просто в массив «чего угодно»), в этом случае a[i] = null не подходит. Надо либо сдвигать элементы, либо писать специальный маркер в несуществующие позиции (например, a[i] = 0). Конкретный подход зависит от назначения конкретного массива и причин, по которым нужно из него удалять элементы.
                    +6
                    А точно в Backbone есть dispose? Потому что в свежей версии такой функции нет.

                    Большая проблема со статьями такого рода — они все уже давно устарели. Конкретно в этой статье предлагается набор советов от 2012 года. Уже 3 года прошло, большинство из этих советов — неактуальны.

                    Предлагаю автору добавить вверху дату оригинальной статьи, иначе на текущий момент многие из советов можно отнести к разряду «вредных».
                      0
                      AFAIK в Backbone никогда не было функции/метода с таким именем. Возможно это опечатка. Не знаю, что там имел в виду Эдди, но по смыслу там подходит stopListening.
                      0
                      В примере с генераций таблицы и добавления евентов есть косяк кажется. Переменная frag2 должна пересоздаваться внутри цикла for потому что сейчас каждая следующая строка таблицы будет содержать все td из предыдущих рядов.
                        0
                        Статья хорошая, но хотелось бы побольше услышать про оптимизацию именно при работе с DOM, т.к. какой бы быстрой не была функция и какой бы эффективной не была сборка мусора и распределение памяти — всё это сходит на нет, если мы собираемся много работать с DOM. В тексте статьи по этому поводу сказано фактически лишь одно — не работайте с DOM в цикле, для этого надо использовать documentFragment. Но что, если сам по себе documentFragment является очень большим, например, таблица 1000 строк по 5 колонок? У меня встраивание такого фрагмента вызывало небольшие подвисания браузера (приблизительно 0.5 с), какие бы рекомендации вы могли дать?
                          +1
                          Тормозит даже не работа с DOM — браузеры делают всё, чтобы ускорить в том числе этот аспект (где-то даже использование DocumentFragment не даёт ускорения), а частые изменения в DOM, особенно, когда это приводит к пересчёту стилей. Ведь при этом надо обойти каждый элемент, который потенциально мог быть задет, и проверить нужны ли изменения в оформлении и применить их, если нужны.

                          В примере с таблицей стоит отметить, что таблица сама по себе сложный элемент для оформления. По умолчанию браузеры стараются автоматически распределить текст по ширине так, чтобы таблица вместилось по ширине. Причём содержимое ячейки (а оно может быть каким угодно) не может выйти за пределы этой ячейки. Каждая ячейка может повлиять на внешний вид всей таблицы, а значит каждая должна ячейка должна быть рекурсивно обработана, причём может потребоваться не один проход. Скорее всего, у вас данные имеют вполне определённый вид, и заранее можно сказать как должна быть оформлена таблица. Нужно использовать table-layout: fixed и задать ширину таблице и всем колонкам¹. Тогда браузер сможет сразу отобразить таблицу, избежав сложных вычислений.

                          ¹ Впрочем, ширину колонок указывать необязательно, можно воспользоваться тем фактом, что не будучи указанной ширина между ними будет распределена поровну. Ширина колонок указывается через ширину ячеек в первом ряду или элементы <col>.
                            +1
                            Я всегда думал, что браузер делает пересчет уже на следующем «тике», когда текущий фрагмент js-кода «отпустил» однопоточный интерпретатор и управление вернулось назад в ядро js. Т.е. вставите вы в DOM 1 элемент или 100 — все равно пересчет будет выполнен 1 раз, когда произойдет выход из всех запущенных вложенных функций. Я так думал. Но это, похоже, неверно в силу замечаний про DocumentFragment? Поясните, пожалуйста.

                            Даже метод такой был: выполнять какие-то вещи на setTimeout(..., 1), т.е. на следующем событийном цикле, когда браузер пересчитал ширины-высоты.
                              0
                              советую почитать про дерево перерисовки и работу селекторов, чтобы понимать про DOM лучше. Можно также соответствующую главку из книжки
                              speedupyourwebsite.ru/books/reactive-websites/
                              +4
                              Есть и непонимание по поводу того, как работает null. Установка ссылки на объект в null не обнуляет объект. Писать o.x = null лучше, чем использовать delete, но смысла это не имеет.

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

                              Если эта ссылка была последней ссылкой на объект, его затем приберёт сборщик мусора. Если это была не последняя ссылка, до него можно добраться, и сборщик его не подберёт.
                              Переведённый автор здесь противоречит сам себе: мысль, изложенная до фрагмента кода, фактически противоречит мысли, изложенной после него.

                              Верно написано, что переменная является всего лишь ссылкою на объект. Но именно поэтому её и зануляют (обращают в null), чтобы не оставалось лишних ссылок на объект и начал работать сборщик мусора, подчистив объект. Если переменная является частью области видимости некоторого замыкания, то ссылка на объект может жить очень долго, если её не занулить — именно так и возникают «утечки памяти».
                                –1
                                Если кратко:

                                1. Не используйте переменные.
                                2. Если очень надо их использовать, хотя бы не записывайте в них значения и нигде не используйте.
                                  +1
                                  При создании таблицы, мне кажется лишним добавление ячеек во фрагмент, почему их сразу не добавлять в элемент строки, ведь элемент строки находится во фрагменте и добавление ячеек в строки не вызовет перерисовки. Так что
                                  frag2.appendChild(td);
                                  
                                  мне кажется лишними телодвижениями.
                                    0
                                    «простить исправить» — красиво звучит :-)

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое