Optimize JavaScript, simple trick and techniques - Part 1
When I come across a huge codebase, I often feel that it can be optimised without changing the core of the programme. I'm listing a set of techniques that I have found useful in the past. In computers, every solution has a trade-off; mostly, performance optimisation comes with a trade-off of reliability, but if it saves you significant resources, you may want to make a trade-off.
Avoid string comparisons
JavaScript authors attempt to hide complexities, and one such complexity is hidden behind ===
operator. In native languages like C, you get a strcmp
function that iterates over string length and compares characters on each index, This complexity of iterating over the string is hidden behind a simple looking ===
operator.
Consider the following examples:
// Example: 1 String Compare
enum Number {
EVEN = "EVEN",
ODD = "ODD"
}
// String compare
let count = 0;
for (let i = 0; i < 1000000; i++) {
const label = i % 2 === 0 ? Number.EVEN : Number.ODD;
// "EVEN" === "EVEN"
// "ODD" === "EVEN"
// 1. iterate over string to compare
// 2. pass by ref: mem lookups
if (label === Number.EVEN) {
count += 1;
}
}
The above code is a comparison of strings, which requires iteration over string length. Also, to a JS developer, the strings are passed by value, but internally, the strings in the engine are passed by reference, and those are stored in the string pool, i.e., the heap, which requires a memory lookup on each step. However, in the example below, we are using intergers, which are passed by value.
// Example: 2 Integer compare
enum Number {
EVEN = 0,
ODD = 1
}
// String compare
let count = 0;
for (let i = 0; i < 1000000; i++) {
const label = i % 2 === 0 ? Number.EVEN : Number.ODD;
// 0 === 0
// 1 === 0
// 1. simple byte integer compare
if (label === Number.EVEN) {
count += 1;
}
}
Avoid different shapes
JavaScript engines are optimised for same-shaped objects. Object { x, y }
and { y, x }
are of different shapes and thus are not very well optimised. In the case of many properties, the more variation an individual object has, the more wild it looks to engine and less optimisation kicks in.
function add(a, b) {
return {
x: a.x + b.x,
y: a.y + b.y
}
}
const a = { x: 10, y: 5 };
const b = { y: 11, x: 6 }; // shape of a and b are diff
// had `b` been { x, y } shape would be same and optimised
Use the factory function to create objects that will ensure that they have the same shapes.
Avoid Array and Object methods
Belive me I love functional programming but my standards vary a bit. No matter how much I love .map, .filter, .reduce
, if I see consuemtive usage of array methods, I will try to write it in an imperative way.
const result = [1.5, 3.5, 5.0]
.map((n) => Math.round(n))
.filter(n => n % 2)
.reduce((a, n) => a + n, 0);
The problem is that it creates a new array each time, and multiple iterations happen on the array. I'm okay with writing new imperative code and putting it in a function as below.
function getRoundedEvenSum(array) {
let sum = 0;
for (let i = 0; i < array.length; i++) {
const num = array[i];
const roundedNum = MAth.round(num);
if (roundedNum % 2 === 0) {
sum += roundedNum;
}
}
return sum;
}
// complexity is being one function
// and it is still okishly readable
same fate occursObject.values
, Object.keys
and Object.enteries
they creates new array in memory, which can be costly as an object grows.
Nested objects
Usually, it is good idea to avoid nested objects and accesseing deeply nested properties. Caching the nested object keys early can help reduce nested lookups. Although engines are good at it, it may cost you. The same way JS proxies are a bit more heavy on lookups, try to avoid them if you can.
const nested = { a: { b: { c: { x: 10, y: 11 } } } };
// costly
loop many times {
nested.a.b.c.x + nested.a.b.c.y
}
// cache early
const x = nested.a.b.c.x;
const y = nested.a.b.c.y;
loop many times {
nested.a.b.c.x + nested.a.b.c.y
}
Let's understand these very well and follow up in our next article. Stay tuned more to come in series.