V8 is Google’s engine for compiling our JavaScript. Firefox has it’s own engine called SpiderMonkey, it’s quite similar to V8 but there are differences. We will be discussing the V8 engine in this article.
A couple of facts about the V8 engine:
So what exactly happens when we send our JavaScript to be parsed by the V8 engine (this is after it is minified, uglified and whatever other crazy stuff you do to your JavaScript code)?
I’ve created the following diagram that shows all the steps, we will then discuss each step in detail:
In this article we’ll discuss how the JavaScript code gets parsed and how to get as much of your JavaScript to the Optimizing Compiler as possible. The Optimizing Compiler (aka Turbofan) takes our JavaScript code and converts it to high performance Machine Code, so the more code we can give it the faster our application will be. As a side note, the interpreter in Chrome is called Ignition.
So the first treatment of our JavaScript code is to parse it. Let’s discuss exactly what parsing is.
There are two phases to parsing which are:
Which is better? It all depends.
Let’s look at some code.
// eager parse declarations right away
const a = 1;
const b = 2;
// lazily parse this as we don't need it right away
function add(a, b) {
return a + b;
}
// oh looks like we do need add so lets go back and parse it
add(a, b);
So here our variable declarations will be eager parsed
but then our function is lazily parsed
. This is great until we get to add(a, b)
as we need our add
function right away so it would have been quicker to eager parse
add
right away.
To eager parse
the add
function right away, we can do:
// eager parse declarations right away
const a = 1;
const b = 2;
// eager parse this too
var add = (function(a, b) {
return a + b;
})();
// we can use this right away as we have eager parsed
// already
add(a, b);
This is how most modules you use are created.
Chrome will sometimes essentially rewrite your JavaScript, one example of this is inlining a function that is being used.
Let’s take the following code as an example:
const square = (x) => { return x * x }
const callFunction100Times = (func) => {
for(let i = 0; i < 100; i++) {
// the func param will be called 100 times
func(2)
}
}
callFunction100Times(square)
The above code will be optimized by the V8 engine as follows:
const square = (x) => { return x * x }
const callFunction100Times = (func) => {
for(let i = 100; i < 100; i++) {
// the function is inlined so we don't have
// to keep calling func
return x * x
}
}
callFunction100Times(square)
As you can see from the above, V8 is essentially removing the step where we call func
and instead inlining the body of square
. This is very useful as it will improve the performance of our code.
There is a little gotcha with this approach, let’s take the following code example:
const square = (x) => { return x * x }
const cube = (x) => { return x * x * x }
const callFunction100Times = (func) => {
for(let i = 100; i < 100; i++) {
// the function is inlined so we don't have
// to keep calling func
func(2)
}
}
callFunction100Times(square)
callFunction100Times(cube)
So this time after we have called the square
function 100
times, we will then call the cube
function 100
times. Before cube
can be called, we must first de-optimize the callFunction100Times
as we have inlined the square
function body. In cases like this, the square
function will seem like it’s faster than the cube
function but what is happening is the de-optimization step makes the execution longer.
When it comes to objects, V8 under the hood has a type system to differentiate your objects:
The objects have the same keys with no differences.
// mono example
const person = { name: 'John' }
const person2 = { name: 'Paul' }
The objects share a similar structure with some small differences.
// poly example
const person = { name: 'John' }
const person2 = { name: 'Paul', age: 27 }
The objects are entirely different and cannot be compared.
// mega example
const person = { name: 'John' }
const building = { rooms: ['cafe', 'meeting room A', 'meeting room B'], doors: 27 }
So now we know the different objects in V8, let’s see how V8 optimizes our objects.
Hidden classes are how V8 identifies our objects.
Let’s break this down into steps.
We declare an object:
const obj = { name: 'John'}
V8 will then declare a classId
for this object.
const objClassId = ['name', 1]
Then our object is created as follows:
const obj = {...objClassId, 'John'}
Then when we access the name
property on our object like so:
obj.name
V8 does the following lookup:
obj[getProp(obj[0], name)]
This is process V8 goes through when creating our objects, now let’s see how we can optimize our objects and reuse classIds
.
If you can, you should declare your properties in the constructor. This will ensure the object structure stays the same so V8 can then optimize your objects.
class Point {
constructor(x,y) {
this.x = x
this.y = y
}
}
const p1 = new Point(11, 22) // hidden classId created
const p2 = new Point(33, 44)
You should keep the property order constant, take the following example:
const obj = { a: 1 } // hidden class created
obj.b = 3
const obj2 = { b: 3 } // another hidden class created
obj2.a = 1
// this would be better
const obj = { a: 1 } // hidden class created
obj.b = 3
const obj2 = { a: 1 } // hidden class is reused
obj2.b = 3
So now let’s get into some general tips that will help your JavaScript code be better optimized.
When arguments are being passed to a function it’s important they are the same type. Turbofan will give up trying to optimize your JavaScript after 4 tries if the argument types are different.
Take the following example:
function add(x,y) {
return x + y
}
add(1,2) // monomorphic
add('a', 'b') // polymorphic
add(true, false)
add({},{})
add([],[]) // megamorphic - at this stage, 4+ tries, no optimization will happen
Another tip is to make sure to declare classes in the global scope:
// don't do this
function createPoint(x, y) {
class Point {
constructor(x,y) {
this.x = x
this.y = y
}
}
// new point object created every time
return new Point(x,y)
}
function length(point) {
//...
}
So I hope you learned a few things about how V8 works under the hood and how to write better optimized JavaScript code.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
So I think some of these points, especially the ones to do with function inlining might be off now that it’s 2022. I did the following test and found that the later two with side effects were faster, but I’ll have to try some other methods such as changing them to objects to really see what can go wrong
Just a typo i guess, If
was optimized into
it should lead to serious problems since the return keyword will stop the outer for loop at the first iteration… and this is not what we expected from the source code.