Efficient Default Property Values in TypeScript

In order to ensure that our website runs as fast and efficiently as possible, we often use performance analysis tools to take a look at what is happening inside the browser, specifically on more constrained devices such as Mobile Phones. It was during one of these profiling sessions that we realised that the JavaScript emitted by the TypeScript compiler had the potential to be optimised so that the output used CPU and Memory more efficiently.

While investigating this we devised a TypeScript design pattern for properties of classes called the Static Initialisation Pattern. This article discusses this pattern, how it was devised and the benefits it delivers.

Static Initialisation Pattern

So what does the Static Initialisation Pattern (also known as Static Init, or SInit for short) look like?

class Test {
    private label: string;

    protected static SInit = (() => {
        Test.prototype.label = "";
    })();
}

Lets break this down:

class Test {
    private label: string;  

This is a TypeScript class with a property which we wish to have a default value.

protected static SInit = (() => {
    Test.prototype.label = "";
})();

Rather than specifying the default value in the property declaration, we add instead a protected static member called SInit, which forces the evaluation of a closure which adds the property to the class prototype with a default value. SInit is declared as static so that it is evaluated at class declaration time and is attached to the class definition, rather than any instances. It is declared as protected so that any derived classes can also declare their own SInit without causing a compilation error.

Key points to note:

  • Class has one or more properties (in this case label)
  • These properties have a default value (empty string "");

If there are more than one property with a default value, the class would look like:

class Rectangle {
    private width: number;
    private height: number;

    protected static SInit = (() => {
      Rectangle.prototype.width = 0;
      Rectangle.prototype.height = 0;
    })();
}

In order to understand why we would use this pattern, lets take a look at how default values work in TypeScript.

TypeScript Default Property Values

If you give properties a default value within the class body using idiomatic TypeScript it will initialise them in the constructor each the time the class is instantiated.

Example 1: Idiomatic TypeScript

class TestA {
    private label = "";
    protected value = 0;
    public list: string[] = null;
}

Example 1: Emitted JavaScript

var TestA = /** @class */ (function () {
    function TestA() {
        this.label = "";
        this.value = 0;
        this.list = null;
    }
    return TestA;
}());

This results in the JavaScript runtime needing to dynamically create and attach properties and assign values each time that the object is created. If there are a large number of properties, and you intend to create a large number of instances of the class, then this will have a significant impact on performance.

With Static Init Pattern

Using the Static Init Pattern, the property is initialised on the prototype only once when the script is loaded, resulting in a more lightweight constructor.

Example 2: TypeScript With Static Init Pattern

class TestB {
    private label: string;
    protected value: number;
    public list: string[];

    protected static SInit = (() => {
        TestB.prototype.label = "";
        TestB.prototype.value = 0;
        TestB.prototype.list = null;
    })();
}

Example 2: Emitted JavaScript

var TestB = /** @class */ (function () {
    function TestB() {
    }
    TestB.SInit = (function () {
        TestB.prototype.label = "";
        TestB.prototype.value = 0;
        TestB.prototype.list = null;
    })();
    return TestB;
}());

Dissecting The Pattern

So why does the pattern look as it does? This is because of the expectations that TypeScript places on the contents of Class definitions. In order for the closure to execute at Class definition time it needs to be evaluated as a static member. Not having the static member definition and attempting to invoke the closure without it results in the following compilation error:

Unexpected token. A constructor, method, accessor, or propert was expected.

Therefore we settled on the convention of the protected static member called SInit.

Within the contents of the closure the pattern relies on the JavaScript concept of prototypal inheritance to ensure that the properties are created with default values on each instance, without the need to set the default value separately on every object instance each time it is created.

Testing

In order to prove that the pattern really did improve performance, we benchmarked the two different sets of JavaScript code and compared the results.

The Benchmark looked like this:

HTML

<ul id='cycleResults'></ul>
<div id="result"></div>
<br>
<button id="btn">Run Tests</button>

JavaScript

var test_runs = 10000;
// TESTS ====================
var TestA = /** @class */ (function() {
  function TestA() {
    this.label = "";
    this.value = 0;
    this.list = null;
  }
  return TestA;
}());
var TestB = /** @class */ (function() {
  function TestB() {}
  TestB.SInit = (function() {
    TestB.prototype.label = "";
    TestB.prototype.value = 0;
    TestB.prototype.list = null;
  })();
  return TestB;
}());
function test1() {
  var instances = [];
  for (var i = 0; i < test_runs; i++) {
    instances.push(new TestA());
  }
}
function test2() {
  var instances = [];
  for (var i = 0; i < test_runs; i++) {
    instances.push(new TestB());
  }
}
var cycleResults = document.getElementById('cycleResults');
var result = document.getElementById('result');
var btn = document.getElementById('btn');
// BENCHMARK ====================
btn.onclick = function runTests() {
  btn.setAttribute('disable', true);
  cycleResults.innerHTML = '';
  result.textContent = 'Tests running...';
  var suite = new Benchmark.Suite();
  // add tests
  suite
    .add('test1', test1)
    .add('test2', test2)
    .on('cycle', function(event) {
      var result = document.createElement('li');
      result.textContent = String(event.target);
      document.getElementById('cycleResults')
        .appendChild(result);
    })
    .on('complete', function() {
      result.textContent = 'Fastest is ' + this.filter('fastest').pluck('name');
      btn.setAttribute('disable', false);
    })
    .run({
      'async': true
    });
};

The Benchmark also uses Benchmark.js (specifically Benchmark.js 1.0.0).

Test Results:

Here are some actual test results from Chrome 60:

test1 x 1,964 ops/sec ±2.82% (90 runs sampled)
test2 x 4,075 ops/sec ±1.21% (85 runs sampled)
Fastest is test2

In these tests, a higher number is better as the test was able to perform more operations per second.

Test Results

As the chart indicates, we see a performance gain from 10% to 100% with this approach across all the major browsers. Don’t take my word for it though – please feel free to test this yourself using this Performance Benchmark.

Memory Usage

Using the Static Init Pattern declares the shape of the prototype definition to the runtime before instances of the object are created, meaning that new instances are declared in a single contiguous block of memory, resulting in more efficient heap usage.

Without the use of this pattern, the different properties of an instance would be allocated in different areas of memory, resulting in slower reads and writes and more memory fragmentation.

In a typical JavaScript Runtime Environment, JavaScript Objects have a model similar to the following diagram:

Figure 1

By declaring the structure of the object at script load time, more of the properties of the object are contained in the “in-object” properties, rather than in the linked properties structure.

The article Fast Properties in V8 discusses this in further detail for the V8 engine used in Google Chrome.

Negatives

The Static Init Pattern does result in slightly larger classes than if it is not used. We found this as an acceptable trade-off as once the output has been minified, combined and gzipped the size increase was negligible.

It also results in an empty property SInit existing on the Class definition (not instances). We have not found this to be an issue.

Considerations

When using the Static Init Pattern, it is important to remember that property values initialised on the prototype are shared across all instances of the object. This is fine when the value is a simple type or constant value, but can cause issues if it is an instance of an object or an array as this effectively becomes a static reference across all objects without being explicitly stated as such. Recommended primitive values would be numbers, null, strings and booleans.

Conclusion

Use of the Static Initialisation Pattern has resulted in a significant performance increase in our usage scenarios, both in terms of efficient CPU and Memory usage. This results in a faster, better performing experience for our users.

Leave a Reply

*Mandatory fields