Поиск…


Вступление

JavaScript, как и любой язык, требует от нас разумного использования определенных функций языка. Чрезмерное использование некоторых функций может снизить производительность, в то время как некоторые методы могут быть использованы для повышения производительности.

замечания

Помните, что преждевременная оптимизация - это корень всего зла. Сначала напишите четкий, правильный код, а затем, если у вас возникли проблемы с производительностью, используйте профилировщик для поиска конкретных областей для улучшения. Не тратьте время на оптимизацию кода, который не влияет на общую производительность значимым образом.

Измерение, измерение, измерение. Производительность часто бывает несовместимой и со временем меняется. То, что сейчас быстрее, может быть не в будущем, и может зависеть от вашего варианта использования. Убедитесь, что какие-либо оптимизации, которые вы делаете, на самом деле улучшаются, а не ухудшают производительность, и что изменение стоит.

Избегайте попыток / улов в критически важных функциях

Некоторые механизмы JavaScript (например, текущая версия Node.js и более ранние версии Chrome перед Ignition + turbofan) не запускают оптимизатор для функций, содержащих блок try / catch.

Если вам нужно обрабатывать исключения в критическом по производительности коде, в некоторых случаях может быть быстрее сохранить try / catch в отдельной функции. Например, эта функция не будет оптимизирована некоторыми реализациями:

function myPerformanceCriticalFunction() {
    try {
        // do complex calculations here
    } catch (e) {
        console.log(e);
    }
}

Однако вы можете реорганизовать перенос медленного кода в отдельную функцию (которая может быть оптимизирована) и вызвать ее из блока try .

// This function can be optimized
function doCalculations() {
    // do complex calculations here
}

// Still not always optimized, but it's not doing much so the performance doesn't matter
function myPerformanceCriticalFunction() {
    try {
        doCalculations();
    } catch (e) {
        console.log(e);
    }
}

Вот тест jsPerf, показывающий разницу: https://jsperf.com/try-catch-deoptimization . В текущей версии большинства браузеров не должно быть большой разницы, если таковая имеется, но в менее поздних версиях Chrome и Firefox или IE версия, которая вызывает вспомогательную функцию внутри try / catch, скорее всего, будет быстрее.

Обратите внимание, что такие оптимизации должны быть сделаны тщательно и с фактическими данными, основанными на профилировании вашего кода. По мере совершенствования движков JavaScript это может привести к ухудшению производительности вместо того, чтобы помогать или вообще не иметь никакого значения (но это не усложняет код). Помогает ли это, болит или не имеет никакого значения, может зависеть от множества факторов, поэтому всегда измеряйте влияние на ваш код. Это справедливо для всех оптимизаций, но особенно таких микро-оптимизаций, которые зависят от низкоуровневых деталей компилятора / времени выполнения.

Использование memoizer для функций большой вычислительной мощности

Если вы строите функцию, которая может быть тяжелой на процессоре (клиентская или серверная), вы можете захотеть рассмотреть memoizer, который является кешем предыдущих исполнений функций и их возвращаемыми значениями . Это позволяет проверить, были ли ранее переданы параметры функции. Помните, что чистыми функциями являются те, которые задают вход, возвращают соответствующий уникальный вывод и не вызывают побочные эффекты вне их объема, поэтому вы не должны добавлять memoizers к непредсказуемым функциям или зависеть от внешних ресурсов (например, AJAX-вызовы или случайным образом возвращаемые значения).

Скажем, у меня есть рекурсивная факториальная функция:

function fact(num) {
  return (num === 0)? 1 : num * fact(num - 1);
}

Если я передам небольшие значения от 1 до 100, то проблем не будет, но как только мы начнем глубже, мы можем взорвать стек вызовов или сделать процесс немного болезненным для механизма Javascript, в котором мы это делаем, особенно если двигатель не учитывает оптимизацию хвостового вызова (хотя Дуглас Крокфорд говорит, что в состав ES6 встроена оптимизация хвостового вызова).

Мы могли бы жестко закодировать наш собственный словарь от 1 до бог-знает-какое число с их соответствующими факториалами, но я не уверен, что я советую это! Давайте создадим memoizer, не так ли?

var fact = (function() {
  var cache = {}; // Initialise a memory cache object
  
  // Use and return this function to check if val is cached
  function checkCache(val) {
    if (val in cache) {
      console.log('It was in the cache :D');
      return cache[val]; // return cached
    } else {
      cache[val] = factorial(val); // we cache it
      return cache[val]; // and then return it
    }
    
    /* Other alternatives for checking are:
    || cache.hasOwnProperty(val) or !!cache[val]
    || but wouldn't work if the results of those
    || executions were falsy values.
    */
  }

  // We create and name the actual function to be used
  function factorial(num) {
    return (num === 0)? 1 : num * factorial(num - 1);
  } // End of factorial function

  /* We return the function that checks, not the one
  || that computes because  it happens to be recursive,
  || if it weren't you could avoid creating an extra
  || function in this self-invoking closure function.
  */
  return checkCache; 
}());

Теперь мы можем начать использовать его:

Вот как это делается

Теперь, когда я начинаю размышлять над тем, что я сделал, если бы я должен увеличивать с 1 вместо декремента от num , я мог бы кэшировать все факториалы от 1 до num в кэше рекурсивно, но я оставлю это для вас.

Это здорово, но что, если у нас есть несколько параметров ? Это проблема? Не совсем, мы можем сделать несколько приятных трюков, таких как использование JSON.stringify () в массиве аргументов или даже список значений, от которых зависит функция (для объектно-ориентированных подходов). Это делается для создания уникального ключа со всеми включенными аргументами и зависимостями.

Мы также можем создать функцию, которая «memoizes» других функций, используя ту же концепцию области, что и раньше (возвращая новую функцию, которая использует оригинал и имеет доступ к объекту кэша):

ПРЕДУПРЕЖДЕНИЕ: синтаксис ES6, если вам это не нравится, заменить ... ничем и использовать var args = Array.prototype.slice.call(null, arguments); обмануть; замените const и пусть с var, и другие вещи, которые вы уже знаете.

function memoize(func) {
  let cache = {};

  // You can opt for not naming the function
  function memoized(...args) {
    const argsKey = JSON.stringify(args);
    
    // The same alternatives apply for this example
    if (argsKey in cache) {
      console.log(argsKey + ' was/were in cache :D');
      return cache[argsKey];
    } else {
      cache[argsKey] = func.apply(null, args); // Cache it
      return cache[argsKey]; // And then return it
    }
  }

  return memoized; // Return the memoized function
}

Теперь обратите внимание, что это будет работать для нескольких аргументов, но не будет иметь большого смысла в объектно-ориентированных методах, я думаю, вам может понадобиться дополнительный объект для зависимостей. Кроме того, func.apply(null, args) можно заменить func(...args) так как деструктурирование массива отправит их отдельно, а не как форму массива. Кроме того, просто для справки, передача массива в качестве аргумента функции func не будет работать, если вы не используете Function.prototype.apply как я.

Чтобы использовать вышеуказанный метод, вы просто:

const newFunction = memoize(oldFunction);

// Assuming new oldFunction just sums/concatenates:
newFunction('meaning of life', 42);
// -> "meaning of life42"

newFunction('meaning of life', 42); // again
// => ["meaning of life",42] was/were in cache :D
// -> "meaning of life42"

Бенчмаркинг вашего кода - измерение времени выполнения

Большинство рекомендаций по производительности очень зависят от текущего состояния двигателей JS и, как ожидается, будут актуальны только в данный момент времени. Основным законом оптимизации производительности является то, что вы должны сначала измерить, прежде чем пытаться оптимизировать и снова измерять после предполагаемой оптимизации.

Чтобы измерить время выполнения кода, вы можете использовать различные инструменты измерения времени, такие как:

Производительность интерфейс , который представляет собой временную информацию , относящиеся к производительности для данной страницы (доступно только в браузерах).

process.hrtime на Node.js дает вам информацию о времени в виде [секунд, наносекунд] кортежей. Вызывается без аргумента, он возвращает произвольное время, но вызывается с ранее возвращенным значением в качестве аргумента, он возвращает разницу между двумя исполнениями.

console.time("labelName") консоли console.time("labelName") запускают таймер, который вы можете использовать для отслеживания продолжительности операции. Вы даете каждому таймеру уникальное имя ярлыка и может иметь до 10 000 таймеров, запущенных на данной странице. Когда вы вызываете console.timeEnd("labelName") с тем же именем, браузер завершает таймер для заданного имени и выводит время в миллисекундах, прошедшее с момента запуска таймера. Строки, переданные во время (), и timeEnd () должны совпадать, иначе таймер не закончит.

Date.now функция Date.now() возвращает текущую метку времени в миллисекундах, который является количество представление времени с 1 января 1970 00:00:00 UTC до сих пор. Метод now () является статическим методом Date, поэтому вы всегда используете его как Date.now ().

Пример 1 с использованием: performance.now()

В этом примере мы собираемся рассчитать прошедшее время для выполнения нашей функции, и мы будем использовать метод Performance.now (), который возвращает DOMHighResTimeStamp , измеренный в миллисекундах, с точностью до одной тысячной миллисекунды.

let startTime, endTime;

function myFunction() {
    //Slow code you want to measure
}

//Get the start time
startTime = performance.now();

//Call the time-consuming function
myFunction();

//Get the end time
endTime = performance.now();

//The difference is how many milliseconds it took to call myFunction()
console.debug('Elapsed time:', (endTime - startTime));

Результат в консоли будет выглядеть примерно так:

Elapsed time: 0.10000000009313226

Использование функции performance.now() имеет самую высокую точность в браузерах с точностью до одной тысячной миллисекунды, но с самой низкой совместимостью .

Пример 2 : Date.now()

В этом примере мы собираемся рассчитать прошедшее время для инициализации большого массива (1 миллион значений), и мы будем использовать метод Date.now()

let t0 = Date.now(); //stores current Timestamp in milliseconds since 1 January 1970 00:00:00 UTC
let arr = []; //store empty array
for (let i = 0; i < 1000000; i++) { //1 million iterations
   arr.push(i); //push current i value
}
console.log(Date.now() - t0); //print elapsed time between stored t0 and now

Пример 3 : console.time("label") и console.timeEnd("label")

В этом примере мы выполняем ту же задачу, что и в примере 2, но мы будем использовать методы console.time("label") и console.timeEnd("label")

console.time("t"); //start new timer for label name: "t"
let arr = []; //store empty array
for(let i = 0; i < 1000000; i++) { //1 million iterations
   arr.push(i); //push current i value
}
console.timeEnd("t"); //stop the timer for label name: "t" and print elapsed time

Пример 4 с использованием process.hrtime()

В программах Node.js это самый точный способ измерения затраченного времени.

let start = process.hrtime();

// long execution here, maybe asynchronous

let diff = process.hrtime(start);
// returns for example [ 1, 2325 ]
console.log(`Operation took ${diff[0] * 1e9 + diff[1]} nanoseconds`);
// logs: Operation took 1000002325 nanoseconds

Предпочитают локальные переменные для глобальных переменных, атрибутов и индексированных значений

Двигатели Javascript сначала ищут переменные в локальной области, прежде чем расширять их поиск до более крупных областей. Если переменная является индексированным значением в массиве или атрибутом в ассоциативном массиве, он сначала ищет родительский массив до того, как он найдет содержимое.

Это имеет значение при работе с критичным по производительности кодом. Возьмем, например, общий for цикла:

var global_variable = 0;
function foo(){
    global_variable = 0;
    for (var i=0; i<items.length; i++) {
        global_variable += items[i];
    }
}

Для каждой итерации в for цикла, двигатель будет искать items , параметры поиск в length атрибута в пределах пунктов, подстановки items снова, LookUp значения по индексу i из items , а затем , наконец , LookUp global_variable , сначала пытается локальная областью видимости перед проверкой глобального масштаба.

Исполняющая функция переписывает указанную выше функцию:

function foo(){
    var local_variable = 0;
    for (var i=0, li=items.length; i<li; i++) {
        local_variable += items[i];
    }
    return local_variable;
}

Для каждой итерации в переписана for цикла, двигатель будет искать li , параметры поиска items , LookUp значения по индексу i и поиск local_variable , на этот раз только необходимость проверять локальную область видимости.

Повторное использование объектов, а не повторное создание

Пример А

var i,a,b,len;
a = {x:0,y:0}
function test(){ // return object created each call
    return {x:0,y:0};
}
function test1(a){ // return object supplied 
    a.x=0;
    a.y=0;
    return a;
}   

for(i = 0; i < 100; i ++){ // Loop A
   b = test();
}

for(i = 0; i < 100; i ++){ // Loop B
   b = test1(a);
}

Петля B равна 4 (400%) раз быстрее, чем Loop A

Это очень неэффективно для создания нового объекта в коде производительности. Loop A call function test() который возвращает новый объект для каждого вызова. Созданный объект отбрасывается на каждую итерацию, Loop B вызывает test1() которая требует, чтобы возвращаемые объекты возвращались. Таким образом, он использует один и тот же объект и избегает выделения нового объекта, а также избыточных ударов GC. (GC не были включены в тест производительности)

Пример B

var i,a,b,len;
a = {x:0,y:0}
function test2(a){
    return {x : a.x * 10,y : a.x * 10};
}   
function test3(a){
    a.x= a.x * 10;
    a.y= a.y * 10;
    return a;
}  
for(i = 0; i < 100; i++){  // Loop A
    b = test2({x : 10, y : 10});
}
for(i = 0; i < 100; i++){ // Loop B
    a.x = 10;
    a.y = 10;
    b = test3(a);                
}

Петля B равна 5 (500%) раз быстрее, чем петля A

Ограничение обновлений DOM

Общей ошибкой, наблюдаемой в JavaScript при запуске в среде браузера, является обновление DOM чаще, чем необходимо.

Проблема здесь в том, что каждое обновление в интерфейсе DOM заставляет браузер повторно отображать экран. Если обновление изменяет макет элемента на странице, весь макет страницы необходимо перераспределить, и это очень тяжело, даже в самых простых случаях. Процесс перерисовывания страницы известен как reflow и может привести к тому, что браузер будет работать медленно или даже перестанет отвечать на запросы.

Следствие обновления документа слишком часто иллюстрируется следующим примером добавления элементов в список.

Рассмотрим следующий документ, содержащий элемент <ul> :

<!DOCTYPE html>
<html>
    <body>
        <ul id="list"></ul>
    </body>
</html>

Мы добавляем 5000 наименований в список за цикл 5000 раз (вы можете попробовать это с большим числом на мощном компьютере, чтобы увеличить эффект).

var list = document.getElementById("list");
for(var i = 1; i <= 5000; i++) {             
    list.innerHTML += `<li>item ${i}</li>`;  // update 5000 times
}

В этом случае производительность может быть улучшена путем пакетной обработки всех 5000 изменений в одном обновлении DOM.

var list = document.getElementById("list");
var html = "";
for(var i = 1; i <= 5000; i++) {
    html += `<li>item ${i}</li>`;
}
list.innerHTML = html;     // update once

Функция document.createDocumentFragment() может использоваться в качестве легкого контейнера для HTML, созданного контуром. Этот метод немного быстрее, чем изменение свойства innerHTML элемента контейнера (как показано ниже).

var list = document.getElementById("list");
var fragment = document.createDocumentFragment();
for(var i = 1; i <= 5000; i++) {
    li = document.createElement("li");
    li.innerHTML = "item " + i;
    fragment.appendChild(li);
    i++;
}
list.appendChild(fragment);

Инициализация свойств объекта с нулевым значением

Все современные JavaScript JIT-компиляторы пытаются оптимизировать код на основе ожидаемых структур объектов. Некоторые подсказки от mdn .

К счастью, объекты и свойства часто «предсказуемы», и в таких случаях их базовая структура также может быть предсказуемой. JIT могут полагаться на это, чтобы сделать предсказуемый доступ быстрее.

Лучший способ сделать объект предсказуемым - определить целую структуру в конструкторе. Поэтому, если вы собираетесь добавить дополнительные свойства после создания объекта, определите их в конструкторе с null . Это поможет оптимизатору предсказать поведение объекта на весь его жизненный цикл. Однако у всех компиляторов есть разные оптимизаторы, и увеличение производительности может быть различным, но в целом хорошая практика - определить все свойства в конструкторе, даже если их значение еще не известно.

Время для некоторых испытаний. В моем тесте я создаю большой массив некоторых экземпляров класса с циклом for. Внутри цикла я назначаю ту же строку всем свойствам объекта «x» перед инициализацией массива. Если конструктор инициализирует свойство «x» значением null, массив всегда обрабатывается лучше, даже если он выполняет дополнительную инструкцию.

Это код:

function f1() {
    var P = function () {
        this.value = 1
    };
    var big_array = new Array(10000000).fill(1).map((x, index)=> {
        p = new P();
        if (index > 5000000) {
            p.x = "some_string";
        }

        return p;
    });
    big_array.reduce((sum, p)=> sum + p.value, 0);
}

function f2() {
    var P = function () {
        this.value = 1;
        this.x = null;
    };
    var big_array = new Array(10000000).fill(1).map((x, index)=> {
        p = new P();
        if (index > 5000000) {
            p.x = "some_string";
        }

        return p;
    });
    big_array.reduce((sum, p)=> sum + p.value, 0);
}


(function perform(){
    var start = performance.now();
    f1();
    var duration = performance.now() - start;

    console.log('duration of f1  ' + duration);


    start = performance.now();
    f2();
    duration = performance.now() - start;

    console.log('duration of f2 ' + duration);
})()

Это результат для Chrome и Firefox.

       FireFox     Chrome
 --------------------------
 f1      6,400      11,400
 f2      1,700       9,600   

Как мы видим, улучшения производительности сильно отличаются между этими двумя.

Будьте последовательны в использовании чисел

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

В этом примере мы будем использовать эту тривиальную функцию, суммирующую элементы массива и выводя время, которое потребовалось:

// summing properties
var sum = (function(arr){
        var start = process.hrtime();
        var sum = 0;
        for (var i=0; i<arr.length; i++) {
                sum += arr[i];
        }
        var diffSum = process.hrtime(start);
        console.log(`Summing took ${diffSum[0] * 1e9 + diffSum[1]} nanoseconds`);
        return sum;
})(arr);

Давайте сделаем массив и суммируем элементы:

var     N = 12345,
        arr = [];
for (var i=0; i<N; i++) arr[i] = Math.random();

Результат:

Summing took 384416 nanoseconds

Теперь давайте сделаем то же самое, но только с целыми числами:

var     N = 12345,
        arr = [];
for (var i=0; i<N; i++) arr[i] = Math.round(1000*Math.random());

Результат:

Summing took 180520 nanoseconds

Суммирование целых чисел заняло половину времени здесь.

Двигатели не используют те же типы, что и в JavaScript. Как вы, наверное, знаете, все числа в JavaScript - это числа с плавающей запятой двойной точности IEEE754, для целых чисел нет специального доступного представления. Но двигатели, когда они могут предсказать, что вы используете только целые числа, могут использовать более компактное и быстрое использование представления, например коротких целых чисел.

Такая оптимизация особенно важна для вычислений или приложений с интенсивным использованием данных.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow