Szukaj…


Wprowadzenie

JavaScript, jak każdy język, wymaga od nas rozsądnego korzystania z niektórych funkcji językowych. Nadużywanie niektórych funkcji może obniżyć wydajność, a niektóre techniki mogą być użyte do zwiększenia wydajności.

Uwagi

Pamiętaj, że przedwczesna optymalizacja jest źródłem wszelkiego zła. Najpierw napisz wyraźny, poprawny kod, a jeśli masz problemy z wydajnością, skorzystaj z narzędzia do profilowania, aby wyszukać określone obszary wymagające poprawy. Nie marnuj czasu na optymalizowanie kodu, który nie wpływa znacząco na ogólną wydajność.

Mierz, mierz, mierz. Wydajność często może być sprzeczna z intuicją i zmienia się w czasie. To, co jest teraz szybsze, może nie być w przyszłości i może zależeć od przypadku użycia. Upewnij się, że wszelkie dokonane przez ciebie optymalizacje faktycznie się poprawiają, nie zmniejszając wydajności, i że zmiana jest opłacalna.

Unikaj prób / catch w funkcjach krytycznych dla wydajności

Niektóre silniki JavaScript (na przykład bieżąca wersja Node.js i starsze wersje Chrome przed Ignition + turbofan) nie uruchamiają optymalizatora w funkcjach zawierających blok try / catch.

Jeśli potrzebujesz obsługiwać wyjątki w kodzie krytycznym dla wydajności, w niektórych przypadkach może być szybsze utrzymanie try / catch w osobnej funkcji. Na przykład ta funkcja nie będzie zoptymalizowana przez niektóre implementacje:

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

Można jednak zmienić fakturę, aby przenieść wolny kod do oddzielnej funkcji (którą można zoptymalizować) i wywołać z wnętrza bloku 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);
    }
}

Oto test porównawczy jsPerf pokazujący różnicę: https://jsperf.com/try-catch-deoptimization . W obecnej wersji większości przeglądarek nie powinno być dużej różnicy, jeśli taka istnieje, ale w mniej nowszych wersjach Chrome i Firefox lub IE wersja, która wywołuje funkcję pomocniczą w try / catch, prawdopodobnie będzie szybsza.

Pamiętaj, że optymalizacje takie jak ta powinny być dokonywane ostrożnie i oparte na faktycznych dowodach opartych na profilowaniu kodu. Ponieważ silniki JavaScript stają się coraz lepsze, może to obniżyć wydajność zamiast pomagać lub w ogóle nie robić żadnej różnicy (ale komplikować kod bez powodu). To, czy to pomaga, boli czy nie robi różnicy, może zależeć od wielu czynników, dlatego zawsze mierz wpływ na twój kod. Dotyczy to wszystkich optymalizacji, ale zwłaszcza takich mikrooptymalizacji, które zależą od szczegółów niskiego poziomu kompilatora / środowiska wykonawczego.

Użyj funkcji memoizer do ciężkich zadań obliczeniowych

Jeśli budujesz funkcję, która może być obciążająca procesor (po stronie klienta lub po stronie serwera), możesz rozważyć użycie memoizera, który jest pamięcią podręczną poprzednich wykonań funkcji i ich zwracanych wartości . Pozwala to sprawdzić, czy parametry funkcji zostały wcześniej przekazane. Pamiętaj, że czyste funkcje to te, które podają dane wejściowe, zwracają odpowiednie unikalne dane wyjściowe i nie powodują skutków ubocznych poza ich zakresem, więc nie należy dodawać notatek do funkcji, które są nieprzewidywalne lub zależą od zasobów zewnętrznych (takich jak wywołania AJAX lub losowo zwrócone wartości).

Powiedzmy, że mam rekurencyjną funkcję czynnikową:

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

Jeśli przekażę na przykład małe wartości od 1 do 100, nie byłoby problemu, ale kiedy zaczniemy wchodzić głębiej, możemy wysadzić stos wywołań lub sprawić, że proces będzie trochę bolesny dla silnika JavaScript, w którym to robimy, zwłaszcza jeśli silnik nie liczy się z optymalizacją ogona (chociaż Douglas Crockford twierdzi, że natywna ES6 ma również optymalizację ogona).

Moglibyśmy na stałe zakodować własny słownik od 1 do boga-wie-jaką liczbę z odpowiadającymi silnikami, ale nie jestem pewien, czy to doradzę! Stwórzmy memoizer, prawda?

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; 
}());

Teraz możemy zacząć go używać:

Tak to się robi

Teraz, gdy zacznę zastanawiać się nad tym, co zrobiłem, gdybym miał zwiększać od 1 zamiast zmniejszania od wartości num , mógłbym buforować wszystkie silnie od 1 do num w pamięci podręcznej rekurencyjnie, ale zostawię to dla ciebie.

To świetnie, ale co, jeśli mamy wiele parametrów ? To jest problem? Nie do końca, możemy wykonać kilka ciekawych sztuczek, takich jak użycie JSON.stringify () na tablicy argumentów lub nawet lista wartości, od których będzie zależeć funkcja (dla podejść obiektowych). Ma to na celu wygenerowanie unikalnego klucza z uwzględnieniem wszystkich argumentów i zależności.

Możemy również utworzyć funkcję, która „zapamiętuje” inne funkcje, korzystając z tej samej koncepcji zakresu jak poprzednio (zwracając nową funkcję, która korzysta z oryginału i ma dostęp do obiektu pamięci podręcznej):

OSTRZEŻENIE: Jeśli nie podoba ci się składnia ES6, zamień ... na nic i użyj var args = Array.prototype.slice.call(null, arguments); sztuczka; zamień const i daj na var i inne rzeczy, które już znasz.

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
}

Teraz zauważ, że to zadziała dla wielu argumentów, ale nie będzie zbyt przydatne w metodach obiektowych. Myślę, że możesz potrzebować dodatkowego obiektu dla zależności. Również func.apply(null, args) można zastąpić func(...args) ponieważ destrukcja tablicy wyśle je osobno, a nie jako formę tablicy. Również w celach informacyjnych przekazywanie tablicy jako argumentu func nie będzie działać, chyba że użyjesz Function.prototype.apply tak jak ja.

Aby skorzystać z powyższej metody, wystarczy:

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"

Benchmarking twojego kodu - pomiar czasu wykonania

Większość wskazówek dotyczących wydajności jest bardzo zależna od aktualnego stanu silników JS i oczekuje się, że będą one istotne tylko w danym momencie. Podstawową zasadą optymalizacji wydajności jest to, że musisz najpierw dokonać pomiaru przed próbą optymalizacji i dokonać pomiaru ponownie po założonej optymalizacji.

Aby zmierzyć czas wykonania kodu, możesz użyć różnych narzędzi do pomiaru czasu, takich jak:

Interfejs wydajności reprezentujący informacje o wydajności związane z czasem dla danej strony (dostępne tylko w przeglądarkach).

process.hrtime na Node.js daje informacje o taktowaniu jako krotki [sekundy, nanosekundy]. Wywołany bez argumentu zwraca dowolny czas, ale wywołany z poprzednio zwróconą wartością jako argumentem zwraca różnicę między dwoma wykonaniami.

Timery console.time("labelName") uruchamia licznik, którego można użyć do śledzenia czasu console.time("labelName") operacji. Nadajesz każdemu licznikowi unikalną nazwę etykiety i może on mieć do 10 000 liczników działających na danej stronie. Po wywołaniu console.timeEnd("labelName") o tej samej nazwie przeglądarka zakończy licznik dla podanej nazwy i wyświetli czas w milisekundach, który upłynął od uruchomienia licznika. Ciągi przekazane do time () i timeEnd () muszą się zgadzać, w przeciwnym razie licznik czasu się nie skończy.

Date.now funkcja Date.now() zwraca bieżący datownik w milisekundach, który jest Ilość reprezentacja czasu od dnia 1 stycznia 1970 00:00:00 UTC aż do teraz. Metoda now () jest statyczną metodą Date, dlatego zawsze używasz jej jako Date.now ().

Przykład 1 z użyciem: performance.now()

W tym przykładzie obliczymy czas, jaki upłynął do wykonania naszej funkcji, i użyjemy metody Performance.now () , która zwraca DOMHighResTimeStamp , mierzoną w milisekundach, z dokładnością do jednej tysięcznej milisekundy.

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));

Wynik w konsoli będzie wyglądał mniej więcej tak:

Elapsed time: 0.10000000009313226

Zastosowanie performance.now() ma najwyższą precyzję w przeglądarkach z dokładnością do jednej tysięcznej milisekundy, ale najniższą kompatybilność .

Przykład 2 z użyciem: Date.now()

W tym przykładzie Date.now() czas, jaki upłynął do zainicjowania dużej tablicy (1 milion wartości), i użyjemy metody 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

Przykład 3 z użyciem: console.time("label") i console.timeEnd("label")

W tym przykładzie wykonujemy to samo zadanie, co w przykładzie 2, ale będziemy używać metod console.time("label") i 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

Przykład 4 za pomocą process.hrtime()

W programach Node.js jest to najbardziej precyzyjny sposób mierzenia spędzonego czasu.

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

Preferuj zmienne lokalne od globalnych, atrybutów i wartości indeksowanych

Silniki JavaScript najpierw szukają zmiennych w zasięgu lokalnym, a następnie rozszerzają wyszukiwanie na większe zakresy. Jeśli zmienna jest wartością indeksowaną w tablicy lub atrybutem w tablicy asocjacyjnej, najpierw szuka tablicy nadrzędnej, zanim znajdzie zawartość.

Ma to wpływ na pracę z kodem krytycznym dla wydajności. Weźmy na przykład wspólną pętlę for :

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

Dla każdej iteracji w pętli for , silnik będzie wyszukiwał items , sprawdzał atrybut length w elementach, ponownie wyszukiwał items , sprawdzał wartość w indeksie i items , a następnie w końcu sprawdzał global_variable , najpierw sprawdzając zasięg lokalny, zanim sprawdził zasięg globalny.

Wykonawczym przepisaniem powyższej funkcji jest:

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

Dla każdej iteracji w przepisanej pętli for silnik będzie wyszukiwał li , szukał items , szukał wartości w indeksie i oraz local_variable zmiennej lokalnej, tym razem tylko sprawdzając zasięg lokalny.

Ponownie użyj obiektów, a nie odtwórz

Przykład A

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);
}

Pętla B jest 4 (400%) razy szybsza niż Pętla A.

Tworzenie nowego obiektu w kodzie wydajności jest bardzo nieefektywne. Pętla Wywołuje funkcję test() która zwraca nowy obiekt przy każdym wywołaniu. Utworzony obiekt jest odrzucany przy każdej iteracji, Pętla B wywołuje test1() który wymaga dostarczenia zwrotów obiektu. W ten sposób wykorzystuje ten sam obiekt i unika przydzielania nowego obiektu oraz nadmiernych trafień GC. (GC nie zostały uwzględnione w teście wydajności)

Przykład 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);                
}

Pętla B jest 5 (500%) razy szybsza niż pętla A.

Ogranicz aktualizacje DOM

Częstym błędem występującym w JavaScript podczas uruchamiania w środowisku przeglądarki jest aktualizacja DOM częściej niż to konieczne.

Problem polega na tym, że każda aktualizacja interfejsu DOM powoduje ponowne renderowanie ekranu przez przeglądarkę. Jeśli aktualizacja zmienia układ elementu na stronie, cały układ strony musi zostać ponownie obliczony, a to jest bardzo obciążające pod względem wydajności, nawet w najprostszych przypadkach. Proces ponownego rysowania strony jest znany jako ponowne wlanie tekstu i może powodować powolne działanie przeglądarki lub nawet brak reakcji.

Konsekwencje zbyt częstego aktualizowania dokumentu ilustruje poniższy przykład dodawania elementów do listy.

Rozważ następujący dokument zawierający element <ul> :

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

Dodajemy 5000 pozycji do listy zapętlając 5000 razy (możesz spróbować z większą liczbą na silnym komputerze, aby zwiększyć efekt).

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

W takim przypadku wydajność można poprawić, grupując wszystkie 5000 zmian w jednej aktualizacji 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

Funkcji document.createDocumentFragment() można użyć jako lekkiego kontenera dla HTML utworzonego przez pętlę. Ta metoda jest nieco szybsza niż modyfikowanie właściwości innerHTML elementu kontenera (jak pokazano poniżej).

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);

Inicjowanie właściwości obiektu wartością null

Wszystkie nowoczesne kompilatory JavaScript JIT próbujące zoptymalizować kod na podstawie oczekiwanych struktur obiektowych. Jakaś wskazówka od MDN .

Na szczęście obiekty i właściwości są często „przewidywalne”, aw takich przypadkach ich podstawowa struktura może być również przewidywalna. Zespoły JIT mogą na tym polegać, aby przyspieszyć przewidywalny dostęp.

Najlepszym sposobem uczynienia obiektu przewidywalnym jest zdefiniowanie całej struktury w konstruktorze. Jeśli więc chcesz dodać dodatkowe właściwości po utworzeniu obiektu, zdefiniuj je w konstruktorze o null . Pomoże to optymalizatorowi przewidzieć zachowanie obiektu w całym cyklu jego życia. Jednak wszystkie kompilatory mają różne optymalizatory, a wzrost wydajności może być inny, ale ogólnie dobrą praktyką jest definiowanie wszystkich właściwości w konstruktorze, nawet jeśli ich wartość nie jest jeszcze znana.

Czas na testy. W moim teście tworzę dużą tablicę niektórych instancji klas za pomocą pętli for. W pętli przypisuję ten sam ciąg do właściwości „x” wszystkich obiektów przed inicjalizacją tablicy. Jeśli konstruktor inicjuje właściwość „x” z wartością null, tablica zawsze przetwarza lepiej, nawet jeśli wykonuje dodatkową instrukcję.

To jest kod:

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);
})()

Jest to wynik dla Chrome i Firefox.

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

Jak widzimy, ulepszenia wydajności różnią się bardzo między nimi.

Bądź konsekwentny w użyciu liczb

Jeśli silnik jest w stanie poprawnie przewidzieć, że używasz określonego małego typu dla swoich wartości, będzie w stanie zoptymalizować wykonany kod.

W tym przykładzie użyjemy tej trywialnej funkcji sumującej elementy tablicy i wyliczającej czas potrzebny:

// 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);

Zróbmy tablicę i zsumuj elementy:

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

Wynik:

Summing took 384416 nanoseconds

Teraz zróbmy to samo, ale tylko z liczbami całkowitymi:

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

Wynik:

Summing took 180520 nanoseconds

Sumowanie liczb całkowitych zajęło tutaj połowę czasu.

Silniki nie używają tego samego typu, co w JavaScript. Jak zapewne wiesz, wszystkie liczby w JavaScript są liczbami zmiennoprzecinkowymi podwójnej precyzji IEEE754, nie ma konkretnej dostępnej reprezentacji liczb całkowitych. Ale silniki, jeśli potrafią przewidzieć, że używasz tylko liczb całkowitych, mogą użyć bardziej zwartej i szybszej w użyciu reprezentacji, na przykład krótkich liczb całkowitych.

Tego rodzaju optymalizacja jest szczególnie ważna w przypadku obliczeń lub aplikacji intensywnie przetwarzających dane.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow