Secret Behind JavaScript Performance: V8 & Hidden Classes

How JavaScript Achieved C++ Performance

Chameera Dulanga
Bits and Pieces

--

Today, JavaScript has become one of the most used languages for web development. However, it has to pass so many hurdles to climb up to this stage. One such milestone is behind its execution speed, where it achieved similar performance to languages like C++.

None of these is possible without the invention of the V8 JavaScript Engine.

So, in this article, I will discuss the technology behind these performance gains, and what’s you should know about to write better-performing code.

What is V8 & How It Works

V8 is an open-source JavaScript engine introduced by Google. It is written in C++ and supports Google Chrome, Chromium web browsers, and NodeJS. It is responsible for interacting with the environment and generating bytecode to run the programs.

Initially, V8 was introduced as a performance improvement mechanism for web browsers, and over time, it became a much more improved interpreter than any other engine.

The most significant difference between V8 and other engines is its Just In Time (JIT) compiler.

JIT compiler compiles all the JavaScript to machine code at the run time and does not generate any intermediate code.

High-level architecture of V8 engine

As you can see in the above diagram, the V8 engine consists of 2 main parts. The first part is responsible for parsing your code interpreting it into bytecode, and the latest version of V8 uses an interpreter named Ignition for this process. It will take the abstract syntax tree (AST) generated by the parser as the input and generate the bytecode.

But, we all know that compilers are much faster than interpreters. So then why V8 engine uses an interpreter instead of a compiler?

The main reason for using the Ignition interpreter is to reduce memory usage. This is because the interpreter will only compile necessary lines, unlike a compiler that compiles the whole program.

However, this Ignition interpreter is only responsible for running your code for the very first time. Then, the generated bytecode will be used by a compiler called Turbofan. It will optimize your code based on data it receives during the code execution and recompiles a more optimized version.

Note: Although V8 is used to optimize JavaScript, it is written in C++, and it uses a multi-threaded approach to manage all these work at once.

When I explain how V8 works, I mentioned that the Ignition interpreter takes an abstract syntax tree as the input, so let’s see what an abstract syntax tree is and how it helps V8 improve JavaScript performance.

Tip: Build applications differently

OSS Tools like Bit offer a new paradigm for building modern apps.

Instead of developing monolithic projects, you first build independent components. Then, you compose your components together to build as many applications as you like. This isn’t just a faster way to build, it’s also much more scalable and helps to standardize development.

An independently source-controlled and shared “card” component (on the right, its dependency graph, auto-generated by Bit)

Abstract Syntax Tree

Abstract syntax trees are used to build an abstract structure of the source code for the compiler. Also, it is not specialized for JavaScript or V8; almost every programming language use ASTs to convert high-level code representations to low-level representation.

When you convert your code to an AST, it will include necessary details of the code like variable types, locations, order of the statements, etc. So, your compiler will not have to deal with unnecessary stuff like comments.

To get a better understanding, let’s take a simple JavaScript code and generate AST for that:

// Function declaration
function addition(x, y){
var answer = x + y;
console.log(answer);
}
// Calling the function
addition(10,20);

Then I used the online parsing tool provided by esprima to generate AST for this code. The following code snippet shows a part of the AST, and you can find the full AST of the code from here.

{
“type”: “Program”,
“body”: [
{
“type”: “FunctionDeclaration”,
“id”: {
“type”: “Identifier”,
“name”: “addition”
},
“params”: [
{
“type”: “Identifier”,
“name”: “x”
},
{
“type”: “Identifier”,
“name”: “y”
}
],
“body”: {
“type”: “BlockStatement”,
“body”: [
... ],
“kind”: “var”
},
... “sourceType”: “script”
}

AST defines key values pairs for each line of the code. The initial type identifier defines that the AST belongs to a program, and then all the code lines will be defined inside the body, which is an array of objects.

As I mentioned, all the function declarations, variable declarations, names, types are organized line by line, and comments have been neglected.

Apart from its optimization process and the use of AST, V8 uses another trick to improve the performance of JavaScript. So, let’s see what it is and how it works.

Hidden Classes to Optimize JavaScript Code

As we all know, JavaScript is a dynamically types language. This means we can add or remove attributes from objects on the fly.

Changing object attributes on the fly

However, this approach demands more dynamic lookups, which decreases JavaScript performance.

V8 engines use hidden classes to overcome this issue and optimize the JavaScript execution.

How Hidden Classes Work

When you create a new object, the V8 engine will create a new hidden class for that. Then, if you modify that same object by adding a new property, the V8 engine will create a new hidden class with all the properties from the previous class and include the new property.

Let’s take the above example again and see how hidden classes are generated:

So, when I create an empty object (const userObject = {} ), V8 will create a corresponding hidden class (C01) without any offsets.

Then, I will modify that object by adding a new property. (userObject.name = “Chameera” ). Now, the V8 engine will create a new hidden class (C02), inheriting all properties from the previous hidden class (C01), and assign the name attribute to offset 0.

This will allow the compiler to bypass a dictionary lookup when the property name is accessed, and V8 will directly point to class C01.

If I add another property to this object, the same process will happen. Another hidden class will be created, and it will have both previous and new attributes as offsets.

This hidden class concept not only allows you to bypass dictionary lookups; it also allows you to reuse already created classes when similar objects are created or modified.

For example, if you create another empty object called article (const articleObject = {}), the V8 engine will not create a new hidden class. Instead, it will point to the already created C01 class.

But if you modify the articleObject by adding a new property called articleName , V8 won't be able to use the previously created class (C02) since it only has a property named name.

Writing High-Performing JavaScript Code

So, if you want to maximize the performance of your JavaScript code, you might need to reduce the dynamic property addition.

Suppose you are running a loop in NodeJS. If you add dynamic properties for an object, you see a performance difference inside the loop. So it’s better to create the properties outside the loop and use them instead of adding them dynamically inside the loop.

Therefore when V8 reuses the existing hidden classes, it will perform way much better.

Conclusion

Whenever there is a discussion about how JavaScript works, we talk about event loops, micro-tasks, macro-tasks, and callback queues. However, all these things are not implemented in JavaScript. Instead, they are a part of the V8 engine, and it is responsible for optimizing your JavaScript code.

So, that’s why I wanted to discuss how V8 worked and the way it uses the hidden class concept to optimize your code.

I hope you learned something new about JavaScript with this article, and don’t forget to share your thought in the comments section.

Thank you for reading !!!

--

--