Schreiben von effizientem JavaScript (HTML)

Applies to Windows and Windows Phone

Die Schreibweise des JavaScript-Codes kann großen Einfluss auf die Leistung Ihrer App haben. Hier erfahren Sie, wie Sie häufige Fehler vermeiden und Ihre App so aufbauen, dass ihre Leistung optimiert ist.

Vermeiden unnötiger DOM-Interaktionen

In Windows Store-Apps, die eine JavaScript-Plattform verwenden, sind das DOM und das JavaScript-Modul separate Komponenten. Jeder JavaScript-Vorgang, der eine Kommunikation zwischen diesen Komponenten erfordert, ist relativ aufwendig im Vergleich zu Vorgängen, die sich innerhalb der JavaScript-Laufzeit abspielen. Daher ist es wichtig, unnötige Interaktionen zwischen diesen Komponenten zu vermeiden. Durch die sorgfältige Erfassung der DOM-Interaktionen können Sie Ihre App ordentlich beschleunigen. Mit den Tipps in diesem Abschnitt können Sie eine bis zu siebenmal höhere Geschwindigkeit erzielen, wenn sie in Stapeln von 1000 Zyklen ausgeführt werden.

Beispielsweise kann das Abrufen und Festlegen der Eigenschaften von DOM-Elementen recht aufwendig sein. In diesem Beispiel wird wiederholt auf die body-Eigenschaft und mehrere andere Eigenschaften zugegriffen.



// Do not use: inefficient code. 
function calculateSum() {
    // Retrieve Values
    var lSide = document.body.children.content.children.lSide.value;
    var rSide = document.body.children.content.children.rSide.value;

    // Generate Result
    document.body.children.content.children.result.value = lSide + rSide;
}

function updateResultsStyle() {
    if (document.body.children.content.children.result.value > 10) {
        document.body.children.result.class = "highlighted";
    } else {
        document.body.children.result.class = "normal";
    }
}


Im nächsten Beispiel ist der Code optimiert: durch Zwischenspeichern des Werts der DOM-Eigenschaften statt wiederholtem Zugriff.


function calculateSum() {
    // Retrieve Values
    var contentNodes  = document.body.children.content.children;
    var lSide = contentNodes.lSide.value;
    var rSide = contentNodes.rSide.value;

    // Generate Result
    contentNodes.result.value = lSide + rSide;
}

function updateResultsStyle() {
    
    var contentNodes = document.body.children.content.children;
    if (contentNodes.result.value > 10) {
        contentNodes.result.class = "highlighted";
    } else {
        contentNodes.result.class = "normal";
    }
}



Verwenden Sie DOM-Objekte nur zum Speichern von Infos, die direkten Einfluss darauf haben, wie das DOM Elemente anordnet oder einfügt. Wenn die lSide-Eigenschaft und die rSide-Eigenschaft aus dem vorherigen Beispiel nur Infos über den internen Zustand der App enthalten, hängen Sie sich nicht an ein DOM-Objekt an. Im nächsten Beispiel werden ausschließlich JavaScript-Objekte verwenden, um den internen Zustand der App zu speichern. In dem Beispiel werden DOM-Elemente nur dann aktualisiert, wenn die Anzeige aktualisiert werden muss.


var state = {
    lValue: 0,
    rValue: 0,
    result: 0
};

function calculateSum() {
    state.result = state.lValue + state.rValue;
}

function updateResultsStyle() {
    var contentNodes = document.body.children.content.children;
    if (result > 10) {
        contentNodes.result.class = "highlighted";
    } else {
        contentNodes.result.class = "normal";
    }
}




Verwenden von "innerHTML", wenn möglich

Die dynamische Erstellung von DOM-Objekten kann ebenfalls zulasten der Leistung gehen. Beachten Sie, dass jedes neue HTML-Element, das Sie dem DOM hinzufügen, einen Aufruf von document.createElement und HTMLElement.appendChild erfordert. Wenn Sie das Element mit einer ID oder einem Klassenwert versehen, kommen auch noch Aufrufe von HTMLElement.setAttribute hinzu.


// Building DOM elements can require a lot of code ...
var newElement = document.createElement('div');
newElement.setAttribute('id', 'newElement');
newElement.setAttribute('class', 'someClass');
parentElement.appendChild(newElement);

Eine Möglichkeit zum Verbessern der Leistung von dynamisch generierten HTML-Elementen ist die Verwendung von HTMLElement.innerHTML und toStaticHTML. Dadurch kann der stark optimierte HTML-Parser die Zeichenfolge interpretieren und dem DOM hinzufügen. Wenn Sie dem DOM eine größere Anzahl von Elementen hinzufügen, kann die Leistung proportional verbessert werden.



// Adding some HTML to an element quick and easily.
element.innerHTML = toStaticHTML("<div id='newElement' class='someClass'><h1>I'm in a div!</h1></div>");

Dies ermöglicht aber natürlich die Einschleusung von falsch formatiertem oder schädlichem HTML-Code in Ihre App. Es hat sich bewährt, HTMLElement.innerHTML nur zur dynamischen Generierung von DOM-Elementen zu verwenden, die Ihre App direkt kontrolliert. Mit anderen Worten: Es ist keine gute Idee, mit dieser Technik Benutzereingaben innerhalb der App zu rendern.

Optimieren des Zugriffs auf Eigenschaften

In JavaScript gibt es keine Klassen und Objekte, die lediglich als Eigenschaftensammlungen (oder Wörterbücher) dienen. Sie können einzelnen Objekten ohne Umschweife neue Eigenschaften hinzufügen. Sie können sogar Eigenschaften aus vorhandenen Objekten entfernen. Diese Flexibilität ging früher immer mit Leistungseinbußen einher. Da Objekte eine beliebige Anzahl von Eigenschaften in beliebiger Reihenfolge haben können, bedeutete das Abrufen von Eigenschaftswerten eine aufwändige Wörterbuchsuche. Moderne JavaScript-Module beschleunigen den Eigenschaftszugriff für bestimmte häufig verwendete Programmierungsmuster durch die Verwendung eines internen abgeleiteten Typsystems, das Objekten mit dem gleichen Eigenschaftsaufbau einen Typ (eine Zuordnung) zuweist. In den nächsten Abschnitten erfahren Sie, wie Sie sich diese Verbesserungen zunutze machen können.

Hinzufügen aller Eigenschaften in Konstruktoren

Angenommen, Sie haben eine Funktion, mit der die Durchschnittshelligkeit eines Pixelbereichs in einem Bild berechnet wird. Jedes Pixel wird durch ein Color-Objekt mit vier Eigenschaften dargestellt: r, g, b und a. Wenn die a-Eigenschaft fehlt, verwendet die Funktion stattdessen den Wert 255.


function calculateAverageBrightness(pixels) {
    var length = pixels.length;
    var brightness = 0;
    for (var i = 0; i < length; i++) {
        var c = pixels[i];
        brightness += ((c.r + c.g + c.b) / 3 * (c.a | 255) / 255) / length;
    }
    return brightness;
}


Wie schnell die Funktion ausgeführt wird, hängt davon ab, wie Sie die Objekte im pixels-Array erstellen. Durch Festlegen aller Eigenschaften im Konstruktor werden die Objekte im nächsten Beispiel so erstellt, dass der abgeleitete Typ jedes einzelnen (black, white und foggy) gleich ist. Deshalb wird die Schleife in calculateAverageBrightness zügig abgearbeitet.


function Color(r, g, b, a) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
}

var black = new Color(0, 0, 0, 255);
var white = new Color(255, 255, 255, 255);
var foggy = new Color(255, 255, 255, 64);

var a = [white, black, foggy];
var brightness = calculateAverageBrightness(a);



Im nächsten Beispiel werden alle Eigenschaftswerte außer a mit dem Konstruktor festgelegt. Nach Erstellung der Objekte legt er die a-Eigenschaft für eines der Objekte (foggy) fest, nicht aber für die anderen. Die Folge ist, dass die calculateAverageBrightness-Funktion sehr viel langsamer ausgeführt wird. Sie muss eine Wörterbuchsuche für jeden Eigenschaftsabruf durchführen, da jedes Objekt einen anderen Eigenschaftsaufbau hat.



// Do not use: inefficient code. 
function Color(r, g, b, a) {
    this.r = r;
    this.g = g;
    this.b = b;
}

var black = new Color(0, 0, 0);
var white = new Color(255, 255, 255);
var foggy = new Color(255, 255, 255);
foggy.a = 64;

var a = [white, black, foggy];
var brightness = calculateAverageBrightness(a);



Keine Eigenschaften löschen

Es mag nützlich erscheinen, Objekten temporäre Eigenschaften hinzuzufügen und diese später zu löschen, wenn sie nicht mehr benötigt werden. Diese Vorgehensweise ist aber keineswegs zu empfehlen, da sie die Leistung stark herabsetzen kann.

Im nächsten Beispiel wird die temporäre Hilfseigenschaft processed hinzugefügt, um die Verarbeitung eines Color-Objekts nachzuverfolgen. Zwar wird die Eigenschaft im Beispiel nach der Verwendung entfernt, jedoch bleibt die Leistung bei jeder Operation, die für diese Objekte ausgeführt wird, beeinträchtigt.



// Do not use: inefficient code. 
function blur(pixels) {
    var length = pixels.length;
    for (var i = 0; i < length; i++) {
        pixels[getRightNeighbor(i)].processed = true;
        if (!pixels[i].processed) {
            process(pixel[i]);
        }
    }
    for (var i = 0; i < length; i++) {
        delete pixels[i].processed;
    }
}
...
var pixels = getImagePixels();
...
blur(pixels)
...
var brightness = calculateAverageBrightness(pixels);


Verwenden derselben Reihenfolge von Eigenschaften

Wenn Sie Objekte an unterschiedlichen Positionen erstellen, ist es wichtig, dass die Eigenschaften in derselben Reihenfolge hinzugefügt werden. Sonst ist der interne Typ der Objekte nicht identisch und der Eigenschaftenzugriff langsam. In diesem Beispiel werden zwei Pixelobjekte erstellt (white und black), ihre Eigenschaften aber in unterschiedlicher Reihenfolge hinzugefügt. Folglich haben die beiden Objekte nicht den gleichen internen Typ, und der Zugriff auf ihre Eigenschaften erfordert aufwendige Wörterbuch-Suchvorgänge.



// Do not use: inefficient code. 
// Somewhere in the code
var white = {};
white.r = 255;
white.g = 255;
white.b = 255;
white.a = 255;

// Elsewhere in the code
var black = {};
black.a = 0;
black.r = 255;
black.g = 255;
black.b = 255;

var a = [white, black];
var brightness = calculateAverageBrightness(a);

// Somewhere in the code
var white = {r: 255, g: 255, b: 255, a: 255};

// Elsewhere in the code
var black = {a: 0, r: 255, g: 255, b: 255};

var a = [white, black];
var brightness = calculateAverageBrightness(a);


Keine Standardwerte für Prototypen und andere bedingt hinzugefügte Eigenschaften verwenden

Auf Eigenschaften, die für Prototypobjekte definiert wurden, können Sie in der gleichen Weise zugreifen wie auf Eigenschaften, die für Instanzobjekte definiert wurden. Es mag verlockend sein, Standardwerte für bestimmte Eigenschaften von Prototypen zu definieren. Durch die Definition von Standardwerten kann die Speichernutzung verringert werden, da die Eigenschaften nicht in jedem Instanzobjekt repliziert werden müssen. Leider werden Objekten, die auf diese Weise definiert werden, aber innerhalb des JavaScript-Moduls unterschiedliche abgeleitete Typen zugewiesen. Der Zugriff auf die Eigenschaften dieser Objekte wird dadurch verlangsamt.

Im nächsten Beispiel wird ein Color-Konstruktor definiert, der der Objektinstanz nur dann Eigenschaften hinzufügt, wenn diese explizit als Argumente übergeben wurden. Anderenfalls werden die für den Prototypen des Color-Objekts definierten Standardwerte verwendet. Da die meisten Color-Objekte den Standardwert a verwenden, spart dieses Verfahren Speicherplatz. Es verlangsamt aber auch den Zugriff auf die Eigenschaften und setzt dadurch die Leistung der calcualteAverageBrightness-Funktion herab.



// Do not use: inefficient code. 
function Color(r, g, b, a) {
    if (r) this.r = r;
    if (g) this.g = g;
    if (b) this.b = b;
    if (a) this.a = a;
}

Color.prototype = {
    r: 0,
    g: 0,
    b: 0,
    a: 255
}

var white = new Color(255, 255, 255);
var black = new Color();
var foggy = new Color(255, 255, 255, 64);

var a = [white, black, foggy];
var brightness = calculateAverageBrightness(a);



Verwenden eines Konstruktors für große Objekte

Die Pflege des Systems aus abgeleiteten Typen kostet Ressourcen – besonders dann, wenn große Objekte mit vielen Eigenschaften verwendet werden. Deshalb beschränkten JavaScript-Module die zulässige Anzahl Eigenschaften des Objekts. Bei Überschreiten dieser Anzahl wird aus dem Objekt eine Eigenschaftensammlung, und der Eigenschaftenzugriff wird verlangsamt. Der Grenzwert kann relativ niedrig sein und z. B. bei nur 12 oder 16 Eigenschaften liegen. Die Beschränkung wird jedoch flexibler (oder vollständig aufgehoben), wenn alle Eigenschaften im Konstruktor hinzugefügt werden. Daher ist es wichtig, Konstruktoren zur Definition der Eigenschaften großer Objekte zu verwenden.

Ineffizient:



// Do not use: inefficient code.
var largeObject = new Object();
largeObject.p01 = 0;
largeObject.p02 = 0;
...
largeObject.p31 = 0;
largeObject.p32 = 0;


Optimiert:


function LargeObject() {
    this.p01 = 0;
    this.p02 = 0;
...
    this.p31 = 0;
    this.p32 = 0;
}

var largeObject = new LargeObject();



Objekte richtig strukturieren

Fügen Sie Objektinstanzen Eigenschaften, aber dem Objektprototyp Methoden hinzu. Methoden, die Instanzen als interne Funktionen des Konstruktors hinzugefügt werden, stellen Abschlüsse dar, die den Aktivierungskontext des Konstruktors erfassen. Immer dann, wenn ein neues Objekt erstellt wird, muss folglich ein separates Abschlussobjekt für jede dieser Methoden zugeordnet werden.

In diesem Beispiel werden die Methoden im Fall des Vector-Objekts für die Objektinstanz definiert, anstatt für den Objektprototyp. Jedes Mal, wenn ein Vector-Objekt zugeordnet wird, müssen daher zwei zusätzliche Abschlussobjekte erstellt werden. Dadurch wird zusätzlicher Arbeitsspeicher beansprucht und die Leistung herabgesetzt.



// Do not use: inefficient code.
function Vector(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;

    this.magnitude = function () {
        return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
    };

    this.normalize = function () {
        var m = this.magnitude();
        return new Vector(this.x / m, this.y / m, this.z / m);
    }
}


Dieses Muster wird häufig als Kapselungsmechanismus verwendet, um private Eigenschaften und Methoden zu implementieren. Das nächste Beispiel veranschaulicht dies. Kapselung ist zwar grundsätzlich gut, aber der Mehraufwand ist bei diesem Muster einfach zu groß.



// Do not use: inefficient code.
function Vector(x, y, z) {
    var that = this;
    that.x = x;
    that.y = y;
    that.z = z;

    this.magnitude = function () {
        return Math.sqrt((that.x * that.x) + (that.y * that.y) + (that.z * that.z));
    };

    this.normalize = function () {
        var m = that.magnitude();
        return new Vector(that.x / m, that.y / m, that.z / m);
    }
}



Stattdessen empfehlen wir, Methoden für den Prototypen des Objekts zu definieren. Beispiel:


function Vector(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

Vector.prototype = {
    magnitude: function () {
        return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
    },

    normalize: function () {
        var m = this.magnitude();
        return new Vector(this.x / m, this.y / m, this.z / m);
    }
}




Definieren Sie Eigenschaftengetter und -setter für den Objektprototypen. Beispiel:


function Vector(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

Object.defineProperty(Vector.prototype, "magnitude", {
    get: function () { 
        return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
    }
});




Verwenden von Ganzzahlarithmetik, soweit möglich

Wenn Sie es gewohnt sind, mit herkömmlichen imperativen Programmiersprachen zu arbeiten, in denen Ganzzahl- und Gleitkommawerte zwei völlig unterschiedliche Typen zugeordnet werden, ist die Gleichbehandlung aller numerischen Werte in JavaScript für Sie vielleicht neu. In JavaScript sind alle numerischen Werte vom Typ "Number", und alle arithmetischen Operationen müssen der Gleitkommasemantik entsprechen. Dieser Unterschied fällt kaum ins Gewicht, wenn Ihr Code nicht viel rechnen muss. Wenn Ihr Code jedoch durch arithmetische Operationen geprägt ist, kann die unerwartete Gleitkommaarithmetik seine Leistung deutlich mindern. Angenommen, Sie haben eine C#-Klasse namens Point, die eine Pixelposition auf dem Bildschirm darstellt und wie im folgenden Beispiel gezeigt näher an den Ursprung verschoben oder skaliert werden kann:


class Point {
    int x;
    int y;

    public void Translate(int dx, int dy) {
        this.x = this.x + dx;
        this.y = this.y + dy;
    }

    public void Halve() {
        this.x = this.x / 2;
        this.y = this.y / 2;
    }
}


Bei einer Ursprungsposition des Punkts von (5, 7) liegt die neue Position nach Aufrufen der Halve-Methoden bei (2, 3). Wenn Sie diesen Code in den entsprechenden JavaScript-Code im folgenden Beispiel übersetzen, ist die neue Position (2,5, 3,5).


function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.translate = function(dx, dy) {
    this.x = this.x + dx;
    this.y = this.y + dy;
}

Point.prototype.halve = function() {
    this.x = this.x / 2;
    this.y = this.y / 2;
}



Da Ganzzahlarithmetik in den meisten Programmen dominiert, werden moderne JavaScript-Module immer mehr für Ganzzahloperationen optimiert. Insbesondere werden zwei Optimierungen vorgenommen:

  • Sofern möglich, generieren sie Ganzzahloperationen (wann immer das Ergebnis einer bestimmten Operation bei Ganzzahl- und Gleitkommaoperation gleich ist). Denn moderne Prozessoren führen Ganzzahlarithmetik viel schneller als Gleitkommaarithmetik aus.
  • Sie stellen Ganzzahlwerte (im üblicherweise verwendeten Bereich) im Arbeitsspeicher so dar, dass keine Heapzuordnung (Boxing) erforderlich ist. Für andere JavaScript-Objekte (einschließlich Gleitkommazahlen) gilt dies nicht.

Im vorherigen Beispiel verwendet die translate-Methode Ganzzahlarithmetik, wenn der Ursprungspunkt (5, 7) ist.

Leider kann die halve-Methode keine Ganzzahlarithmetik verwenden, weil das Ergebnis anders ausfallen würde. Daher verwendet die halve-Methode zwei langsamere Gleitkommaoperationen. Noch wichtiger: Die beiden resultierenden Werte müssen dem Heap zugeordnet werden (Boxing), bevor sie der x-Eigenschaft und der y-Eigenschaft des Punkts zugeordnet werden können. Wenn die translate-Methode danach für denselben Punkt aufgerufen wird, dessen Koordinaten nun (2,5, 3,5) lauten, führt sie auch Gleitkommaoperationen und Heapzuordnungen aus.

Das nächste Beispiel zeigt, wie unbeabsichtigte Gleitkommaarithmetik sich in einem JavaScript-Programm ausbreiten kann. Um dies zu vermeiden, müssen Sie die JavaScript-Laufzeit explizit anweisen, Ganzzahlarithmetik zu verwenden. Das gilt insbesondere für Divisionen. Da hierzu nicht der int-Typ verwendet werden kann (er existiert in JavaScript nicht), verwenden Sie stattdessen den bitweisen or-Operator:


Point.prototype.halve = function() {
    this.x = (this.x / 2) | 0;
    this.y = (this.y / 2) | 0;
}


Das Ergebnis des bitweisen or-Operators ist eine Ganzzahl. Die JavaScript-Laufzeit generiert dann Ganzzahlcode und vermeidet Heapzuordnungen.

Im vorherigen Beispiel werden Objekteigenschaften verwendet, aber die Empfehlung, Gleitkommaarithmetik zu vermeiden, gilt auch für Operationen mit Arrayelementen. Im folgenden Beispiel wird sichergestellt, dass jeder Wert, der an das Array zurückgegeben wird, eine Ganzzahl ist.


function halveArray(a) {
    for (var i = 0, al = a.length; i < al; i++) {
        a[i] = (a[i] / 2) | 0;
    }
}


Wenn Ihr Code viele Rechenvorgänge ausführt, verwenden Sie so viele ganzzahlbasierte Operationen wie möglich. In manchen Fällen kann der Wechsel zu ganzzahlbasierten Operationen die Codeausführung um 20-40 ms beschleunigen.

Vermeiden von Gleitkommawert-Boxing

Im vorigen Abschnitt wurde das Problem des Gleitkommawert-Boxing angesprochen: Immer dann, wenn ein Gleitkommawert einer Eigenschaft eines Arrayobjekts oder -elements zugewiesen wird, muss er zunächst einem Heap zugeordnet werden. Wenn Ihr Programm viele Gleitkomma-Rechenvorgänge ausführt, kann das Boxing sehr aufwendig sein. Das Boxing lässt sich beim Arbeiten mit Objekteigenschaften nicht vermeiden. Beim Arbeiten mit Arrays können Sie das Boxing durch Verwendung typisierter Arrays umgehen.

Im nächsten Beispiel wird Float32Array zur Darstellung einer Reihe (einfarbiger) Pixel verwendet. Da Gleitkommaarrays (Float32Array und Float64Array) nur Gleitkommawerte speichern können (gemäß ECMAScript-Spezifikation), kann die JavaScript-Laufzeit ohne Boxing/Unboxing auf die Werte zugreifen und sie speichern.


function blur(pixels) {
    var length = pixels.length;
    var blurred = new Float32Array(length);
    for (var i = 2; i < length - 2; i++) {
        blurred[i] = (pixels[i - 2] + 2 * pixels[i - 1] + 4 * pixels[i] + 
            2 * pixels[i + 1] + pixels[i + 2]) / 10;
    }
    return blurred;
}

var pixels = new Float32Array(100);

for (var i = 0; i < 100; i++) {
    pixels[i] = 5 * (i % 5);
}

...

var blurred = blur(pixels);


Umstellen anonymer Funktionen auf globalen Umfang

Häufig werden anonyme Funktionen zum Binden von Ereignishandlern an DOM-Ereignisse, zum Abschließen asynchroner Arbeit (mithilfe von Zusagen oder anderen asynchronen Abschlussmustern) oder zum Verarbeiten von Arrayelementen mit der Array.forEach-Methode verwendet. Bei der Verwendung anonymer Funktionen sollten Sie den Aufwand in Verbindung mit Abschlüssen bedenken.

Ähnlich wie Methoden, die für Objektinstanzen definiert wurden (siehe weiter oben), sind anonyme Funktionen Abschlüsse, die den Aktivierungskontext der einschließenden Funktion erfassen. Bei jedem Aufruf der einschließenden Funktion muss ein neues Abschlussobjekt zugeordnet werden. Wenn die einschließende Funktion häufig aufgerufen wird (wie im folgenden Beispiel) und die anonyme Funktion relativ klein ist, kann der Mehraufwand durch das Zuordnen eines Abschlussobjekts beträchtlich sein. In diesem Beispiel muss ein neues Abschlussobjekt bei jedem Aufruf von incrementPointArray zugeordnet werden.


function Point(x, y) {
    this.x = x;
    this.y = y;
}

var points = [new Point(0, 0), new Point(1, 1), new Point(2, 2)];

function incrementPointArray(points) {
    points.forEach(function (point) {
        point.x++;
        point.y++;
    });
}

function runLoop() {
    for (var i = 0; i < 1000; i++) {
        incrementPointArray(points);
    }
}



Verwandte Themen

Verwalten von Arbeitsspeicher in Windows Store-Apps

 

 

Anzeigen:
© 2014 Microsoft