The Javascript Engine

This is the first post in a series I'll be writing about the Javascript V8 Engine. How it works, how to write good code, and so on.

What is the Javascript Engine?

If we want to talk to a computer, we have to speak to it in a language it understands.

For the computer to actually understand Javascript, we need a javascript engine. This engine effectively translates our javascript code into a language that it can actually understand.

There are many javascript engines (also known as ECMAScript engines). The most common one today is the V8 engine, written by Google in C++, and runs on Chrome and Node. Because the engine is written in C++ it is very performant.

The reason this is known as ECMAScript engines is that ECMAScript is a governing body that standardises javascript so that the way our javascript code works the same across all engines. Imagine how chaotic things would be if every browser reacted to your javascript differently!

How does it work?

The Javascript engine first has to understand the code that it has been given. To do this, it does what is called Lexical Analysis.

One example of this process is: Parser -> AST -> Interpreter -> Bytecode or for more advanced engines,-> Profiler -> Compiler -> Optimised bytecode

Parser: The parser breaks down the code into tokens based on syntax, which then get fed into the Abstract Syntax Tree (AST).

We can demonstrate this process on astexplorer.net which actually shows how the program is broken down for lexical analysis. The AST then passes it on to the interpreter, which will transform the data into a format the machine can understand, such as bytecode.

In programming languages, if an interpreter is used, it will read our code line by line. This is a simple way to make our code machine-readable, which is done in-time on the fly. The benefits of this is that it can be run very quickly. For example, if a server sends some javascript code to our browser, it can be immediately interpreted and run. No need to wait for compilation. This is how javascript used to be run in older browsers. The problem with this is that if we have a large codebase, the interpreter will run the same functions over and over again which leads to the browser slowing down. For example, if we have the following code:


function calculate(x, y) {
	return x + y;
}

for (let i = 0; i < 1000; i++) {
	calculate(15, 24);
}

an interpreter will run that code every single time even if the same result is given.

If we instead use a compiler, it is not going to run on the fly. Instead, it does a first pass over the code to understand what we want to do, and then it takes that code and translates it into a new language. This is then interpreted. The benefits of this is that it actually takes our javascript code and rewrites it into a lower level language and optimises it, meaning it will run more efficiently. It will take a little longer to start up, as it has to first compile and optimise our code, but it will run faster during use. Here is an example of the same code after it is optimised by the compiler:

function calculate(x, y) {
 return x + y;
}

for (let i = 0; i < 1000; i++) {
    39;
}

NOTE:

Almost every modern web developer has used Babel and/or Typescript. Both of these are actually compilers.

Babel takes your modern JS code and translates it into older JS code, making it compatible for older browsers.

Typescript is a superset of Javascript that compiles down to Javascript.

NOTE 2:

You'll often here that Javascript is an interpreted language, but this is not technically true. It depends on the implementation. This was certainly true initially when it was created and the primary engine was SpiderMonkey, but is not necessarily the case today.

Which is better?

So which one is better to use? This really depends on the use case. Both have pros and cons. The compiler will take a little longer to get up and running - but the end result will be faster and smoother for the user. The interpreter will start up much faster, but the end result for the user will be slower.

But what if we could combine the two and get the best of both worlds? This is exactly what Google did with the V8 engine by making their Just In Time Compiler (JIT Compiler).

For the V8 engine, after it runs through its parser and AST, it first runs the code through its interpreter into bytecode. Once that's done, the profiler watches our code as it's running and if it sees the same code being run multiple times, it will pass the code to the compiler and it will modify it to make optimisations. This way we get a very fast startup, but also our javascript code will also run very efficiently.

Knowing how this process works actually lets us write better code for the engines compiler, which will help it to optimise things further.

V8 JIT Compiler optimisations

Some examples of ways we can improve and optimise our code with the V8 Engine in mind are:

  • avoiding functions that call 'eval()' function.
  • writing our function arguments intelligently (such as using parameter destructuring)
  • instead of using 'for in' loops with our objects, using object.keys to iterate through them.
  • avoiding the 'with' statement.
  • avoiding the 'delete' statement (this will often lead to deoptimisations with hidden classes).

Some of the most important aspects of compiling optimisations are 'inline caching' and 'hidden classes'. with inline caching, if we were to write this code.


function findUser(user) {
	return `found ${user.firstname} ${user.lastname}`
}

const userData { 
	firstName: 'Bob'
	lastName: 'Bobson'
}

it would be replaced by

	'found Bob Bobson'

because that is all it needs to do.

Because the V8 engine optimises using 'Hidden Classes', if we had the code:


function Fruit(x, y) {
	this.x = x;
	this.y = y;
}

const obj1 = new Fruit(1,2);
const obj2 = new Fruit(3,4);

// instantiating obj1 and obj2 properties in a different order here will slow down the code, as the compiler actually turns these into hidden classes.
obj1.a = 30;
obj1.b = 100;
obj2.b = 100;
obj2.a = 30;

To optimise this, we could either add the a and b properties to the Fruit function, or we could instantiate each of our objects properties in the same order.

Make sure to check out the second part of this series to dive into the V8 engines call stack and memory heap.