Suche…


Einführung

Wie bei jeder Sprache erfordert JavaScript, dass wir bei der Verwendung bestimmter Sprachfunktionen vernünftig sind. Die übermäßige Verwendung einiger Funktionen kann die Leistung beeinträchtigen, während einige Techniken zur Steigerung der Leistung verwendet werden können.

Bemerkungen

Denken Sie daran, dass vorzeitige Optimierung die Wurzel allen Übels ist. Schreiben Sie zuerst klaren, korrekten Code. Wenn Sie Leistungsprobleme haben, suchen Sie mithilfe eines Profilers nach bestimmten Bereichen, die verbessert werden sollen. Verschwenden Sie keine Zeit damit, Code zu optimieren, der die Gesamtleistung nicht sinnvoll beeinflusst.

Messen, messen, messen. Die Leistung kann oft nicht intuitiv sein und ändert sich im Laufe der Zeit. Was jetzt schneller ist, liegt möglicherweise nicht in der Zukunft und kann von Ihrem Anwendungsfall abhängen. Stellen Sie sicher, dass die von Ihnen vorgenommenen Optimierungen tatsächlich verbessert werden und die Leistung nicht beeinträchtigen, und dass sich die Änderung lohnt.

Vermeiden Sie Try / Catch in leistungskritischen Funktionen

Einige JavaScript-Engines (z. B. die aktuelle Version von Node.js und ältere Versionen von Chrome vor Ignition + turbofan) führen den Optimierer nicht für Funktionen aus, die einen try / catch-Block enthalten.

Wenn Sie Ausnahmen in leistungskritischem Code behandeln müssen, kann es in manchen Fällen schneller sein, Try / Catch in einer separaten Funktion zu halten. Diese Funktion wird beispielsweise von einigen Implementierungen nicht optimiert:

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

Sie können jedoch Refactoring den langsamen Code in eine separate Funktion bewegen (das optimiert werden kann) und nennen Sie es aus dem Inneren des try - Block.

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

Hier ist ein jsPerf-Benchmark, der den Unterschied zeigt: https://jsperf.com/try-catch-deoptimization . In der aktuellen Version der meisten Browser sollte es keinen großen Unterschied geben, aber in weniger aktuellen Versionen von Chrome und Firefox (IE) ist die Version, die eine Hilfsfunktion innerhalb von Try / Catch aufruft, wahrscheinlich schneller.

Beachten Sie, dass derartige Optimierungen sorgfältig und mit tatsächlichen Nachweisen auf der Grundlage der Profilerstellung Ihres Codes durchgeführt werden sollten. Wenn JavaScript-Engines besser werden, kann dies die Leistung beeinträchtigen, anstatt zu helfen oder überhaupt keinen Unterschied zu machen (ohne jedoch den Code zu komplizieren). Ob dies hilft, weh tut oder keinen Unterschied macht, kann von vielen Faktoren abhängen. Messen Sie daher immer die Auswirkungen auf Ihren Code. Dies gilt für alle Optimierungen, insbesondere aber für solche Mikrooptimierungen, die von einfachen Details der Compiler / Runtime abhängen.

Verwenden Sie einen Memoizer für umfangreiche Rechenfunktionen

Wenn Sie eine Funktion erstellen, die für den Prozessor möglicherweise stark ist (entweder clientseitig oder serverseitig), sollten Sie einen Memoizer in Betracht ziehen, der ein Cache früherer Funktionsausführungen und ihrer zurückgegebenen Werte ist . So können Sie überprüfen, ob die Parameter einer Funktion zuvor übergeben wurden. Denken Sie daran, dass reine Funktionen diejenigen sind, die bei einer Eingabe eine entsprechende eindeutige Ausgabe zurückgeben und keine Nebeneffekte außerhalb ihres Gültigkeitsbereichs verursachen. Sie sollten also keine Memoizer zu Funktionen hinzufügen, die unvorhersehbar sind oder von externen Ressourcen abhängen (wie AJAX-Aufrufe oder zufällig) zurückgegebene Werte).

Nehmen wir an, ich habe eine rekursive faktorielle Funktion:

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

Wenn ich zum Beispiel kleine Werte von 1 bis 100 übergebe, wäre das kein Problem, aber wenn wir tiefer in die Tiefe gehen, könnten wir den Aufrufstack in die Luft jagen oder den Prozess für die Javascript-Engine, in der wir dies tun, etwas schmerzhaft machen. vor allem, wenn die Engine nicht mit der Rückrufoptimierung zählt (obwohl laut Douglas Crockford die native ES6 die Rückrufoptimierung beinhaltet).

Wir könnten unser eigenes Wörterbuch von 1 bis gott-weiß-welche Nummer mit den entsprechenden Fakultäten hart codieren, aber ich bin mir nicht sicher, ob ich das rate! Lass uns einen Memoizer erstellen, oder?

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

Jetzt können wir damit anfangen:

So wird es gemacht

Nun, da ich anfange, über das nachzudenken, was ich getan habe, hätte ich, wenn ich von 1 anstatt von num erhöhen würde, alle Fakultäten von 1 bis num im Cache rekursiv zwischengespeichert, aber ich lasse das für Sie.

Das ist großartig, aber was ist, wenn wir mehrere Parameter haben ? Das ist ein Problem? Nicht ganz, wir können ein paar nette Tricks machen, wie z. B. JSON.stringify () für das Arguments-Array oder sogar eine Liste von Werten, von denen die Funktion abhängt (für objektorientierte Ansätze). Dies geschieht, um einen eindeutigen Schlüssel mit allen enthaltenen Argumenten und Abhängigkeiten zu generieren.

Wir können auch eine Funktion erstellen, die andere Funktionen "speichert", indem sie dasselbe Gültigkeitskonzept wie zuvor verwenden (eine neue Funktion zurückgeben, die das Original verwendet und Zugriff auf das Cache-Objekt hat):

WARNUNG: ES6-Syntax: Wenn Sie es nicht mögen, ersetzen Sie ... durch nichts und verwenden Sie var args = Array.prototype.slice.call(null, arguments); Trick; Ersetzen Sie const und mit var und die anderen Dinge, die Sie bereits kennen.

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
}

Beachten Sie nun, dass dies für mehrere Argumente funktioniert, aber in objektorientierten Methoden nicht viel nützen wird. Ich denke, Sie benötigen möglicherweise ein zusätzliches Objekt für Abhängigkeiten. func.apply(null, args) kann auch durch func(...args) da die Zerstörung von Arrays sie separat und nicht als Array-Formular sendet. Darüber hinaus Function.prototype.apply Übergabe eines Arrays als Argument an func nur, wenn Sie Function.prototype.apply wie ich verwenden.

Um die obige Methode zu verwenden, müssen Sie nur:

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 Ihres Codes - Messung der Ausführungszeit

Die meisten Leistungstipps hängen stark vom aktuellen Status von JS-Engines ab und werden voraussichtlich nur zu einem bestimmten Zeitpunkt relevant sein. Das grundlegende Gesetz zur Leistungsoptimierung besteht darin, dass Sie zuerst messen müssen, bevor Sie versuchen, eine Optimierung durchzuführen, und nach einer vermuteten Optimierung erneut messen.

Um die Codeausführungszeit zu messen, können Sie verschiedene Zeitmesswerkzeuge verwenden:

Leistungsschnittstelle , die zeitbezogene Leistungsinformationen für die angegebene Seite darstellt (nur in Browsern verfügbar).

process.hrtime auf Node.js liefert Timing-Informationen als [Sekunden, Nanosekunden] -Tupel. Ohne Argument aufgerufen, gibt es eine beliebige Uhrzeit zurück, aber mit einem zuvor zurückgegebenen Wert als Argument wird die Differenz zwischen den beiden Ausführungen zurückgegeben.

Konsolentimer console.time("labelName") startet einen Timer, mit dem Sie ermitteln können, wie lange ein Vorgang dauert. Sie geben jedem Timer einen eindeutigen Labelnamen und können bis zu 10.000 Timer auf einer bestimmten Seite ausführen. Wenn Sie console.timeEnd("labelName") mit demselben Namen console.timeEnd("labelName") , beendet der Browser den Timer für den angegebenen Namen und gibt die Zeit in Millisekunden aus, die seit dem Start des Timers vergangen ist. Die an time () und timeEnd () übergebenen Zeichenfolgen müssen übereinstimmen, andernfalls wird der Timer nicht beendet.

Date.now Funktion Date.now() liefert aktuelle Zeitstempel in Millisekunden, die eine ist Nummer Darstellung der Zeit seit dem 1. Januar 1970 00.00.00 UTC bis jetzt. Die Methode now () ist eine statische Methode von Date, daher wird sie immer als Date.now () verwendet.

Beispiel 1 mit: performance.now()

In diesem Beispiel werden wir die verstrichene Zeit für die Ausführung unserer Funktion berechnen und die Performance.now () - Methode verwenden, die einen DOMHighResTimeStamp- Wert in Millisekunden zurückgibt , der auf ein Tausendstel einer Millisekunde genau ist.

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

Das Ergebnis in der Konsole sieht ungefähr so ​​aus:

Elapsed time: 0.10000000009313226

Die Verwendung von performance.now() hat in Browsern die höchste Genauigkeit mit einer Genauigkeit von einem Tausendstel einer Millisekunde, jedoch die geringste Kompatibilität .

Beispiel 2 using: Date.now()

In diesem Beispiel werden wir die verstrichene Zeit für die Initialisierung eines großen Arrays (1 Million Werte) berechnen und die Date.now() -Methode verwenden

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

Beispiel 3 using: console.time("label") & console.timeEnd("label")

In diesem Beispiel führen wir dieselbe Aufgabe wie in Beispiel 2 aus, verwenden jedoch die console.time("label") und 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

Beispiel 4 mit process.hrtime()

In Node.js-Programmen ist dies der genaueste Weg, die verbrauchte Zeit zu messen.

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

Bevorzugen Sie lokale Variablen globalen Werten, Attributen und indizierten Werten

Javascript-Engines suchen zuerst nach Variablen im lokalen Bereich, bevor sie ihre Suche auf größere Bereiche ausdehnen. Wenn die Variable ein indizierter Wert in einem Array oder ein Attribut in einem assoziativen Array ist, wird zuerst nach dem übergeordneten Array gesucht, bevor der Inhalt gefunden wird.

Dies hat Auswirkungen bei der Arbeit mit leistungskritischem Code. Nehmen Sie zum Beispiel eine allgemeine for Schleife:

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

Für jede Iteration in for Schleife wird der Motor - Lookup items , Nachschlag die length Attribut innerhalb Elemente, Lookup - items wieder, Nachschlag den Wert bei Index i der items , und dann endlich Nachschlag global_variable zuerst den lokalen Bereich versuchen , bevor Sie den globalen Bereich zu überprüfen.

Eine performante Umschreibung der obigen Funktion ist:

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

Für jede Iteration in der umgeschriebenen for Schleife local_variable die Engine nach li , nach items , nach dem Wert am Index i und nach local_variable , diesmal muss nur der lokale Gültigkeitsbereich überprüft werden.

Objekte wiederverwenden statt neu erstellen

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

Schleife B ist viermal (400%) schneller als Schleife A

Es ist sehr ineffizient, ein neues Objekt im Leistungscode zu erstellen. Loop A ruft die Funktion test() die bei jedem Aufruf ein neues Objekt zurückgibt. Das erstellte Objekt wird bei jeder Iteration verworfen. Loop B ruft test1() , für das die test1() bereitgestellt werden muss. Es verwendet daher dasselbe Objekt und vermeidet die Zuweisung eines neuen Objekts sowie übermäßige GC-Treffer. (GC wurde nicht in den Leistungstest einbezogen)

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

Schleife B ist 5 (500%) schneller als Schleife A

Begrenzen Sie DOM-Updates

Ein häufiger Fehler, der in JavaScript bei der Ausführung in einer Browser-Umgebung auftritt, ist, dass das DOM häufiger als nötig aktualisiert wird.

Das Problem hierbei ist, dass der Browser bei jeder Aktualisierung der DOM-Benutzeroberfläche den Bildschirm erneut rendert. Wenn ein Update das Layout eines Elements auf der Seite ändert, muss das gesamte Seitenlayout neu berechnet werden. Dies ist selbst in den einfachsten Fällen sehr leistungslastig. Der Vorgang des erneuten Zeichnens einer Seite wird als Rückfluss bezeichnet und kann dazu führen, dass ein Browser langsam läuft oder gar nicht mehr reagiert.

Die Folge einer zu häufigen Aktualisierung des Dokuments wird anhand des folgenden Beispiels veranschaulicht, in dem Elemente zu einer Liste hinzugefügt werden.

Betrachten Sie das folgende Dokument, das ein <ul> -Element enthält:

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

Wir fügen 5000 Einträge zur Liste hinzu, die 5000 Mal wiederholt werden (Sie können dies mit einer größeren Anzahl auf einem leistungsfähigen Computer versuchen, um den Effekt zu erhöhen).

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

In diesem Fall kann die Leistung verbessert werden, indem alle 5000 Änderungen in einem einzigen DOM-Update zusammengefasst werden.

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

Die Funktion document.createDocumentFragment() kann als leichter Container für den von der Schleife erstellten HTML-Code verwendet werden. Diese Methode ist etwas schneller als das Ändern der innerHTML Eigenschaft des Containerelements (siehe unten).

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

Objekteigenschaften mit null initialisieren

Alle modernen JavaScript-JIT-Compiler versuchen, den Code basierend auf den erwarteten Objektstrukturen zu optimieren. Ein Tipp von mdn .

Glücklicherweise sind die Objekte und Eigenschaften oft "vorhersagbar", und in solchen Fällen kann ihre zugrunde liegende Struktur auch vorhersehbar sein. JITs können sich darauf verlassen, um vorhersagbare Zugriffe schneller zu machen.

Um Objekte vorhersehbar zu machen, definieren Sie am besten eine ganze Struktur in einem Konstruktor. Wenn Sie also nach der Objekterstellung zusätzliche Eigenschaften hinzufügen möchten, definieren Sie sie in einem Konstruktor mit null . Dies hilft dem Optimierer, das Verhalten des Objekts über seinen gesamten Lebenszyklus vorherzusagen. Alle Compiler verfügen jedoch über unterschiedliche Optimierer. Die Leistungssteigerung kann unterschiedlich sein. Im Allgemeinen ist es jedoch ratsam, alle Eigenschaften in einem Konstruktor zu definieren, selbst wenn deren Wert noch nicht bekannt ist.

Zeit für ein paar Tests. In meinem Test erstelle ich ein großes Array einiger Klasseninstanzen mit einer for-Schleife. Innerhalb der Schleife weise ich der "x" - Eigenschaft aller Objekte vor der Initialisierung des Arrays dieselbe Zeichenfolge zu. Wenn der Konstruktor die "x" -Eigenschaft mit null initialisiert, wird das Array immer besser verarbeitet, auch wenn eine zusätzliche Anweisung ausgeführt wird.

Dies ist der Code:

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

Dies ist das Ergebnis für Chrome und Firefox.

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

Wie wir sehen können, sind die Leistungsverbesserungen zwischen den beiden sehr unterschiedlich.

Seien Sie konsequent in der Verwendung von Zahlen

Wenn die Engine richtig vorhersagen kann, dass Sie einen bestimmten kleinen Typ für Ihre Werte verwenden, kann der ausgeführte Code optimiert werden.

In diesem Beispiel verwenden wir diese triviale Funktion, die die Elemente eines Arrays aufsummiert und die Zeit ausgibt, die es gebraucht hat:

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

Lassen Sie uns ein Array erstellen und die Elemente zusammenfassen:

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

Ergebnis:

Summing took 384416 nanoseconds

Jetzt machen wir dasselbe, aber nur mit ganzen Zahlen:

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

Ergebnis:

Summing took 180520 nanoseconds

Das Summieren von ganzen Zahlen nahm hier die Hälfte der Zeit in Anspruch.

Engines verwenden nicht dieselben Typen wie in JavaScript. Wie Sie wahrscheinlich wissen, sind in JavaScript alle Zahlen IEEE754-Gleitkommazahlen mit doppelter Genauigkeit. Es gibt keine spezifische Darstellung für Ganzzahlen. Wenn Engines jedoch vorhersagen können, dass Sie nur Ganzzahlen verwenden, können sie eine kompaktere und schneller zu verwendende Darstellung verwenden, beispielsweise kurze Ganzzahlen.

Diese Art der Optimierung ist besonders wichtig für rechner- oder datenintensive Anwendungen.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow