다음을 통해 공유


Writing efficient JavaScript (HTML)

How you write your JavaScript code can have a big impact on your app's performance. Learn how to avoid common mistakes and how to architect your app for performance.

Avoid unnecessary DOM interactions

In the Windows Store app using JavaScript platform, the DOM and the JavaScript engine are separate components. Any JavaScript operation that involves communication between these components is relatively expensive when compared to operations that can be carried out purely within the JavaScript runtime. So it's important to avoid unnecessary interactions between these components. Carefully accounting for DOM interactions can really speed up your app—the tips shown in this section can provide a 7x speed increase when performed in batches of 1000 cycles.

For example, getting or setting properties of DOM elements can be fairly expensive. This example accesses the body property and several other properties repeatedly.

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

The next example improves the code by caching the value of the DOM properties instead of repeatedly accessing them.

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

Use DOM objects only to store info that directly affects how the DOM lays out or draws elements. If the lSide and rSide properties from the previous example store only info about the internal state of the app, don't attach them to a DOM object. The next example uses pure JavaScript objects to store the internal state of the app. The example updates DOM elements only when the display needs to be updated.

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

Use innerHTML when appropriate

Building DOM objects dynamically can be expensive for performance, too. Consider that any new HTML element that you add to the DOM requires a call to document.createElement and HTMLElement.appendChild. If you add an ID or a class value to the element, you add additional calls to HTMLElement.setAttribute as well.

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

One way to improve the performance of dynamically generated HTML is through the use of HTMLElement.innerHTML and toStaticHTML. This allows the highly optimized HTML parser to interpret the string and add it to the DOM. As you add a larger number of elements to the DOM, this can make a proportionally larger improvement in performance.

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

Of course, this makes it possible for poorly-formed or malicious HTML to be injected into your app. It is a best practice to use HTMLElement.innerHTML only when dynamically generating DOM elements that your app has direct control over. In other words, it's not a good idea to use this technique to render user input within the app.

Optimize property access

In JavaScript there are no classes and objects are simply property bags (or dictionaries). You can add new properties to individual objects on the fly. You can even remove properties from existing objects. This flexibility used to come at a significant price in performance. Because objects can have any number of properties in any order, every property value retrieval required an expensive dictionary lookup. Modern JavaScript engines greatly speed up property access for certain common programming patterns by using an internal inferred type system that assigns a type (or map) to objects of the same property makeup. The next sections tell you how to take advantage of these optimizations.

Add all properties in constructors

Suppose you have a function that calculates the average brightness of a range of pixels in an image. Each pixel is represented by a Color object that has four properties: r, g, b, and a. If the a property is missing, the function uses the value 255 instead.

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

How you construct the objects in the pixels array determines how fast the function runs. By setting all the properties in the constructor, the next example creates the objects so that each one (black, white and foggy) share the same inferred type. As a result, the loop in calculateAverageBrightness completes quickly.

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

The next example uses the constructor to set all the property values except for a. After the objects are created, it sets the a property on one of the objects (foggy) but not the others. As a result, the calculateAverageBrightness function performs much more slowly. It must perform a dictionary lookup for each property retrieval, because each object has a different property makeup.

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

Don't delete properties

It might seem useful to add temporary properties to objects and delete them later after you don't need them anymore. But this is a bad practice because it can greatly degrade performance.

The next example adds a temporary helper property, processed, to track when a Color object has been processed. Even though the example removes the property after it's done using it, the modification continues to degrade the performance of any operations on these objects.

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

Use the same property order

When you create objects in different places, it's important to add properties in the same order. Otherwise, the objects won't share internal types, and property access is slow. This example creates two pixel objects, white and black, but adds their properties in different orders. As a result, the two objects don't share the same internal type and accessing their properties requires expensive dictionary lookups.

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

Don't use default values on prototypes and other conditionally added properties

You can access properties defined on prototype objects just like you access properties defined on instance objects. It might be tempting to define default values for certain properties on prototypes. Defining default values can reduce memory consumption because the properties don't have to be replicated in every instance object. Unfortunately, objects defined this way receive different inferred types inside the JavaScript engine. As a result, accessing the properties of these objects is slow.

The next example defines a Color constructor that adds properties to the object instance only if they were explicitly passed as arguments. Otherwise, the default values defined on the Color object's prototype are used. Because most of the Color objects use the default a, this approach saves memory. But it also slows down property access, so it degrades the performance of the calcualteAverageBrightness function.

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

Use a constructor for large objects

Maintaining the inferred type system costs resources, particularly for large objects that have many properties. Consequently, JavaScript engines typically impose a limit on the number of properties allowed on the object before the object becomes a property bag and provides slow property access. The property limit might be fairly low (such as just 12 or 16 properties). But the limit is relaxed (or lifted entirely) when all properties are added in the constructor. Therefore, it's important to use constructors to define properties when creating large objects.

Bad code:

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

Better code:

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

var largeObject = new LargeObject();

Structure objects correctly

Add properties to object instances, but add methods to the object prototype. Methods added to instances as inner functions of the constructor become closures that capture the activation context of the constructor. Consequently, a new closure object must be allocated for every such method, every time a new object is constructed.

This example defines the methods for the Vector object on the object instance, rather then the object prototype. As a result, every time a Vector object is allocated, two additional closure objects must be created, which costs additional memory and decreases performance.

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

This pattern is often used as an encapsulation mechanism to implement private properties and methods, as shown in the next example. Even though encapsulation is good in principle, the performance overhead makes this pattern impractical.

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

The recommended style is to define methods on the object's prototype, as shown in this example:

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

Define property getters and setters on the object's prototype, as shown in this example:

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

Use integer arithmetic where possible

If your background is in traditional imperative programming languages, in which integer and floating point values have two distinctly different types, you might be surprised by JavaScript's uniform treatment of all numeric values. In JavaScript, all numeric values are of type Number, and all arithmetic operations must follow floating point semantics. This distinction doesn't have much impact if your code doesn't do much arithmetic. On the other hand, if your code is dominated by arithmetic operations, unexpected floating point math can substantially hamper its performance. Suppose you have a C# class named Point that represents a pixel location on the screen and is capable of moving or scaling itself closer to the origin, as in this example:

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

If the point's original location was (5, 7) when the Halve methods is called, the new location is (2, 3). But if you translated this code to the JavaScript equivalent (shown in the next example), the point's new location would be (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;
}

Because integer arithmetic is far more common in most programs, modern JavaScript engines do their best to optimize for integer operations. Specifically, they perform two optimizations:

  • They generate integer operations whenever possible (that is, whenever the result of a given operation is the same for an integer or floating point operation), because modern processors execute integer arithmetic much faster than floating point arithmetic.
  • They represent integer values (in the commonly used range) in memory in such a way that don't require allocation on the heap (boxing), unlike any other JavaScript objects (including floating point numbers).

In the previous example, if the original point is (5, 7), the translate method uses integer arithmetic.

Unfortunately, the halve method can't use integer arithmetic because the result would be different. Consequently, the halve method uses two slower floating point operations. More importantly, the two resulting values must be boxed (allocated on the heap) before they can be assigned to the point's x and y properties. Finally, if the translate method was subsequently called on the same point, whose coordinates would then be (2.5, 3.5), it would also perform floating point operations and heap allocations.

The next example demonstrates how unintended floating point arithmetic can spread throughout a JavaScript program. To avoid it, explicitly tell the JavaScript runtime to use integer arithmetic, particularly when performing division. Since you can't convey this intent by using the int type (it doesn't exist in JavaScript), use the bitwise or operator instead:

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

The result of the bitwise or operator is an integer, so the JavaScript runtime knows to generate integer code and avoid heap allocations.

The previous example used object properties, but the same recommendation for avoiding floating point math applies to operating on array elements. The next example ensures that every value written back to the array is an integer.

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

If your code performs a lot of arithmetic, keep as many operations integer-based as possible. In some cases, switching to integer-based operations can speed up code execution by 20-40 ms.

Avoid floating point value boxing

The previous section mentioned the problem of floating point value boxing: any time a floating point number is assigned to a property of an object or an element of an array, it must first be boxed (allocated on the heap). If your program performs a lot of floating point math, boxing can be costly. You can't avoid boxing when working with object properties, but when operating on arrays, you can use typed arrays to avoid boxing.

The next example uses Float32Array to represent a row of (monochromatic) pixels. Because floating point arrays (Float32Array and Float64Array) can store only floating point values (as defined in the ECMAScript specification), the JavaScript runtime can access and store the values without boxing or unboxing them.

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

Move anonymous functions to global scope

It's common to use anonymous functions to bind event handlers to DOM events, complete asynchronous work (via promises or other asynchronous completion patterns), or process array items using the Array.forEach method. When using anonymous functions, it's good to be aware of the cost associated with closures.

Much like methods defined on object instances (earlier in this document), anonymous functions are closures that capture the activation context of the enclosing function. A new closure object must be allocated every time the enclosing function is called. Consequently, if the enclosing function is called often (as in the next example) and the anonymous function is relatively small, the overhead of allocating a closure object can be significant. In this example, a new closure object must be allocated every time incrementPointArray is called.

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

Managing Memory in Windows Store Apps