Leveraging Hidden Classes To Write Efficient Javascript Code

Note: This article assumes that you are familiar with the basics of Javascript including objects, constructors and dynamic typing.

Understanding the nooks of how Javascript works under the hood is the key to make your application stand out from the rest. Even a few milliseconds of latency can turn out to be a deal-breaker for a user who lands on you website, eventually hurting the business. Keeping this in mind, let's dive into how we can write good code to leverage the internal optimizations of the engine instead of going against them.

For the sake of this blog, we will understand the optimizations with respect to the V8 engine that powers Google Chrome and Node.js.

What to expect?

In this article, you will understand how to write efficient code based on how the JS engine performs optimizations under the hood.

Understanding Hidden Classes

It is important to understand the concept of hidden classes before we can discuss optimizations surrounding them.

Imagine you have a dog and you give him a treat on every two whistles. After a few days, the dog will build up the association of the sound with "ah, it's treat time". Now imagine that you make two whistles but this time you decide not to give the treat. This will leave your dog all baffled and you will have to train him all over again regarding what two whistles actually indicate. (On a serious note, please give him the treat).

The V8 engine provides hidden classes to optimize the access time of retrieving a property from an object. Let's say we created a cake constructor that takes flavour as argument.

function cake(flavour){
    this.flavour= flavour;
}
let chocolateCake = new cake("chocolate");

On invoking the function, we get an object. Let's call it chocolateCake. Initially, a hidden class, say c0, gets created. But when this.flavour= flavour is encountered, a new hidden class, say c1 , will get created and it will contain a memory offset of flavour. Here's when a class transition is added to c0 stating that if you add a property flavour, then the hidden class should transition from c0 to c1. Now, let's create two new cakes.

let vanillaCake= new cake("vanilla");
let redVelvetCake= new cake("redVelvet");

vanillaCake.topping = "nutmeg";
vanillaCake.frosting = "buttercream";

redVelvetCake.frosting = "cream-cheese";
redVelvetCake.topping = "white chips";

The above code will work perfectly fine, but it is not optimized. Take a guess why? The order in which the new properties get instantiated is different for both vanillaCake and redVelvetCake. Therefore two different hidden classes will be created instead of sharing a common hidden class between them.

So even though redVelvetCake and vanillaCake started off with the same hidden class, changing the order of properties will confuse the engine and it will now start treating the new objects differently. Internally, this will slow things down.
Now that we understand the concept of hidden classes, let's see how we can leverage it while using objects.

Try to assign all properties in the constructor itself

 class Position {
   constructor(x, y) {
     this.x = x;
     this.y = y;
   }
 }

 let p1 = new Position (0, 10);  //hidden class Position created
 let p2 = new Position (20, 30); //shares above hidden class

 p1.z = 55;  // another hidden class Position created

This hinders optimization as the methods accepting Position object will now have to be re-optimized with both hidden classes.

Keep the ordering same across similar objects

As we discussed earlier, changing the order of properties makes the engine think that the objects should be treated differently which causes new hidden classes to be created. Therefore, if you have absolutely have to add properties dynamically outside the constructor, ensure that the ordering remains consistent.

Your function arguments should have consistent datatypes

As Javascript is a dynamically typed language, it is very flexible in terms of the datatypes. Although, for performance sake, it is best to not tinker around between different data types for a particular variable. This helps the engine make assumptions about the code and leverage the superpower of hidden classes.

 function addTwoValues(a, b){
    return a + b;
 }
 addTwoValues(100,200) //monomorphic
 addTwoValues("string1", "string2") // changing the data type here so 
 polymorphic
 addTwoValues(true, false) //polymorphic
 addTwoValues({}, {}) //polymorphic
 addTwoValues([], []) //megamorphic

Everytime the type of the arguments is changed, the function is deoptimized and re-optimized. After seeing 4 different changes, the function becomes megamorphic and TurboFan will not optimize the function anymore.

Wrapping it up

Always try to write predictable code to avoid confusing the engine. Instead of asking yourself how you can optimize the code, ask how you can avoid de-optimizations. Avoid using megamorphic functions and instantiate objects correctly. Thank you for reading.

Cheers, Muskan.