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.
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:
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.