Mastering JavaScript high performance
Tags: webperf, javascript, frontend, v8
Published: 2020-06-03 11:26:38

## Intro to JavaScript performance optimization

JavaScript performance optimization techniques are simple to apply if you stick to some "rules" or better-said bits of advice. The implementation of these rules come with a minimum of understanding of how JavaScript engines work today.

---

If you like this article, smile for a moment, share it, follow me, check out my RSS feed and [subscribe to my newsletter](https://marcradziwill.com/newsletter/).

---

For the sake of simplicity, we focus on the V8 the JavaScript engine inside of Chrome and Node.js. The V8 engine offers us passionate performance web developers several possibilities to optimize our JavaScript code.

I posted this article initially on my [blog](https://marcradziwill.com/blog/mastering-javascript-high-performance/).

## Tl;dr

In this article, you read about the JavaScipt engine V8, its main parts for parsing and compiling JavaScript, and how you can help V8 to optimize your JavaScript code. I outline techniques for you to implement.

## Table of Contents

1. [What is a JavaScript engine?](#engine)
   - [Ignition](#ignition)
   - [Turbofan](#turbofan)
   - [The duet of Ignition and Turbofan](#duet)
2. [The Performance Advice](#measure)
3. [How to optimize your JavaScript code](#howopti)
   - [Hidden classes in JavaScript engines](#hiddenclasses)
     - [Takeaways for hidden classes](#takehc)
   - [Inline Caching](#ic)
     - [Monomorphic, Polymorphic, Megamorphic](#mpm)
     - [Takeaways for hidden classes](#takeic)
   - [Take care of JavaScript arrays](#arrays)
     - [Takeaways for arrays](#takearrays)
4. [Conclusion](#conclusion)

## What is a JavaScript engine? <div id="engine"></div>

In simple words, the JavaScript engine converts your code into byte-code or optimized machine-code that runs on your device. The V8 engine has two main parts that play together in this process. As the V8 team choose car part names for the engine, it stuck with that by naming the subprocesses: `Ignition` and `Turbofan`.

### Ignition <div id="ignition"></div>

Ignition is the low-level register-based interpreter. It has a fast start-up, so it's valuable for code executed at page load and rarely executed code in your application. It knows all the JavaScript rules and the ECMAScript standards and won't skip any parts of your code. Therefore the byte-code runs, but it doesn't run fast.

[![Web Performance Checklist](https://dev-to-uploads.s3.amazonaws.com/i/i9nrcwt53letp80a28eg.png)](https://devcademy.marcradziwill.com/web-performance-checklist/)

### Turbofan <div id="turbofan"></div>

Turbofan is the optimization compiler. It generates swift machine code by making optimistic assumptions (we come back to that later). Turbofan is perfect for code after page load and frequently executed code.

But how does this help us to write more efficient JavaScript code? It is the combination of the interpreter and compiler that allows a high-speed JavaScript execution and the best part of it. We can feed the optimization compiler with code that is likely to be optimized.

### The duet of Ignition and Turbofan <div id="duet"></div>

Let's have a quick, simplified look under the hood. After our JavaScript file is loaded and parsed the V8 generates an [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) from it and forwards it to the interpreter which generates byte-code from the AST. As well as Ignition generates byte-code, it also gathers type feedback from the code. Let's assume we have the following simple function.

```javascript
(a) => a + 2;
```

While Ignition executes the code, it gathers type feedback. For our function, this is that `a` is a `Number`. To make it even more simple, we assume that we always pass a number for `a`. The heuristics in Ignition then triggers and marks the function as **hot code** (code with optimization possibilities).

![Pipeline of the JavaScript engine v8](https://marcradziwill.com/static/5c9eaa8964c688cea69db26a1da0f11e/8ec0a/V8-pipeline.jpg 'Pipeline of the JavaScript engine v8')

Ignition then passes the function to Turbofan and provides the type feedback as well. Now it's Turbofans turn. Turbofan makes optimistic assumptions about the code to generate highly optimized machine code. To stay with our example above Turbofan generates machine code for the case that `a` is a number and that code can execute much faster then the byte-code from Ignition could.

But as JavaScript is dynamic, we could call our function with a `String` instead of a `Number`. In this case, the optimistic assumption Turbofan made is wrong, and we have to go back to Ignitions execution.

Let's recap what just happened. First, Ignition passes our code to Turbofan, and the code is optimized. After the assumption was wrong because we added another "type" the code is de-optimized.

So what do we want for the V8 and the other optimization compilers? Code that can be optimized!

## The Performance Advice <div id="measure"></div>

The most useful advice when it comes to performance optimization for your codebase is **measure before you optimize**! As we go through these bits of advice and steps to optimize our JavaScript code, we come across many micro-optimizations. Small parts of your codebase like changing the way you handle arrays access or the initialization of objects. You don't want to change every single line of code and apply all these performance suggestions. You instead need to find the blocks in your application that have a significant influence on your performance (measure) and optimize these parts (optimize).

You won't be happy to go through your codebase of hundreds of thousands lines of code and change everything. You always have to measure before. Take your most popular page and measure where you might have a performance issue related to JavaScript.

## How to optimize your JavaScript code <div id="howopti"></div>

### Hidden classes in JavaScript engines <div id="hiddenclasses"></div>

Hidden classes are the academic term for generating similar shapes of our JavaScript code. Each JavaScript engine has its term. [SpiderMonkey](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey) uses `Shapes`, in [V8](https://v8.dev/) they are called `Maps`, [Chakra](https://github.com/microsoft/ChakraCore) uses `Types` and [JSC](https://developer.apple.com/documentation/javascriptcore) `Structures`. Let's consider the following example:

```javascript
let objectA = {
  x: 1,
  y: 2,
  z: 3,
};

let objectD = {
  x: 1,
  y: 2,
  z: 3,
};
```

Javascript engines generate shapes of each object that you create. If you create similar objects, they share the same shape (Hidden class, Map, Structure, etc.). In the example above, `objectA` shares a shape with `objectB` thus, the engine does not need to allocate additional memory for each object shape. It only needs to store its values. This performance boost is strictly dependent on the order and the way you initialize your objects.

Let's assume we have the code below. If you think of all the objects as a JavaScript developer, there is not much difference. But for JavaScript engines, the order and the way you initialize and declare your objects is significantly essential. In this case, `objectA`, `objectB` and `objectC` won't have the same shape (hidden class), and so V8 can not optimize them concerning their hidden classes. `objectD` instead has the same hidden class as `objectA` and can be accessed faster. V8 knows it's shape/hidden class and can access the stored values in memory faster.

```javascript
let objectA = {
  x: 1,
  y: 2,
  z: 3,
};

let objectB = {};

objectB.x = 1;
objectB.y = 2;
objectB.z = 3;

let objectC = Object.assign({}, objectA);
let objectD = {
  x: 1,
  y: 2,
  z: 3,
};
```

#### Takeaways for hidden classes <div id="takehc"></div>

- Initialize all object members in constructor functions.
- Always initialize object members in the same order.

### Inline Caching <div id="ic"></div>

Shapes or hidden classes enable inline caching. Inline caching is the crucial element to making JavaScript run fast. On an abstract level, inline caching describes that repeated calls on the same method favour to happen on the same objects. Thus V8 caches the type of objects that we pass as a parameter in method calls. Then it uses that information to assume the type of object that we pass as a parameter in the future. If the assumption is true V8 can skip the access to the real object properties in memory and return the cached values instead.

So how are do inline caching and hidden classes work together? Whenever a function is called, V8 looks up the hidden class for that specific object. If the method on that object or an object with the same hidden class is called multiple times, V8 caches the information where to find the object property in memory and returns it instead of looking up the memory itself. Thus on calls in the future V8 can jump directly into the memory address for the object property as long as the hidden class does not change.

That's why it is so essential to implement objects with the same properties in the same order to have the same hidden class. Otherwise, V8 won't be able to optimize your code. In V8 words, you want to stay as much monomorphic as possible. Check out the table below which I found on [a blogpost ](https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/) of Benedikt Meurer where you find the different states for inline caching.

#### Monomorphic, Polymorphic, Megamorphic <div id="mpm"></div>

As JavaScript is very dynamic, you can do many things without thinking about the types. As described above, it's crucial for performance reasons to stay with the same hidden classes that V8 and the other engines can make assumptions to our code. Like I mentioned in the paragraph above there are different states for inline caching. In the table from Benedikt Meurer, you find their definition.

| Marker                                                                                                          | Name           | Description                                                                                       |
| --------------------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| 0                                                                                                               | UNINITIALIZED  | The property access was not executed so far.                                                      |
| .                                                                                                               | PREMONOMORPHIC | The property access was executed once, and we are likely going to go MONOMORPHIC on the next hit. |
| 1                                                                                                               | MONOMORPHIC    | The property access was always executed with the same shape.                                      |
| P                                                                                                               | POLYMORPHIC    | The property access was always executed with one of four different shapes.                        |
| N                                                                                                               | MEGAMORPHIC    | The property access has seen too many different shapes.                                           |
| [source](https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/) |                |

So our goal is to stay as much monomorphic as possible. But what we want to check the inline caching states itself? V8 gives us a possibility with a local build on your machine. To do this, we need to install V8 on our mac, in particular, a debug build of V8 the `d8`. We can follow [the steps on the official V8 site](https://v8.dev/docs/build). For me, as a Mac user, the instructions of [kevincennis on github](https://gist.github.com/kevincennis/0cd2138c78a07412ef21) worked as well.

**_⚠️ Alias setup ⚠️_**: As I used the setup some alias in my bash profile to make the commands of v8 more readable. If you skip this, keep in mind to use the full path to your local V8 installation.

Let's read through the following code. It looks quite similar to the example from inline caching. As you probably already noticed is that I declared two objects with the "same" properties in different ways. First `obj1` is declared and initialized with `x` and `y`. `obj2` only contains property `x` at the beginning and we add the property `y` after the first initialization.

```javascript
function getX(o) {
  return o.x;
}
const obj1 = {
  x: 2,
  y: 4.1,
};

const obj2 = {
  x: 4,
};
obj2.y = 2.2;

let iterations = 1e7;
while (iterations--) {
  getX(obj1);
  getX(obj2);
}
```

As we know, we should avoid this kind of implementation to help V8 optimize our code, and thus we can assume that our code won't be monomorphic. After we installed the debug build of V8, we can use d8 to check the inline caching states. With the following command, we run the code with d8 and pass the parameter to check the inline caching: `$ d8 --trace-ic inline-caching.js`.

d8 now creates a file called `v8.log` which we use to display the inline caches state. We now use the inline caching processor of v8 and our previously generated file `v8.log`. With `$ ic-processor v8.log` the inline caching processor of v8 outputs the states. In our case, we are looking for two lines of code.

```bash
LoadIC (0->1) at ~getX inline-caching.js:2:11 x (map 0x1b6008284ef1)
LoadIC (1->P) at ~getX inline-caching.js:2:11 x (map 0x1b6008284e79)
```

If we compare the states in the output to our overview table, we see that the state first changes from `(0->1)` so from `UNINITIALIZED` to `MONOMORPHIC` and then from `MONOMORPHIC` to `POLYMORPHIC` with `(1->P)`.

Let's make a small change in our code and repeat the steps to check the inline caching again. We change the `obj2` so it has the same hidden class as `obj1`. Now we run `$ d8 --trace-ic inline-caching.js` and `$ ic-processor v8.log` again.

```javascript
const obj2 = {
  x: 4,
  y: 2.2,
};
```

As you see the inline caching state of our code remains monomorphic just by having the same order of object creation.

```bash
LoadIC (0->1) at ~getX inline-caching.js:2:11 x (map 0x22c208284e79)
```

To have a clearer seperation you will find a new JavaScript file called `inline-cachine-opti.js` with the optimized code in the belonging repository.

#### Takeaways for inline caching

- keep the type of parameters safe and don't mix them up.
- Always initialize object members in the same order.

### Take care of JavaScript arrays <div id="arrays"></div>

To understand how arrays work, you need to know that array indexes are handled differently in V8 than objects. Array indexes are stored separately in V8 even if they often behave the same as object properties. V8 calls the stored Array indexes `elements`.

> Objects have properties that match to values and arrays have indexes that map to elements (by Mathias Bynens at SmashingConf London 2018.)

To optimize our arrays the best, V8 keeps track of which kind of element the array contains. V8 differences between 6 kinds of elements.

1. PACKED_SMI_ELEMENTS

```javascript
const array = [1, 2, 3];
```

2. PACKED_DOUBLE_ELEMENTS

```javascript
const array = [1, 2, 3, 4.5];
```

3. PACKED_ELEMENTS

```javascript
const array = [1, 2, 3, 4.5, 'string'];
```

From 1. to 6. the elements stand for different kinds of stored value groups and 1. is more specific where 6. is more generic. Each has its own set of optimization, and you can imagine that the optimization takes longer the more generic it is. `PACKED` means, that the array has no empty entries, `SMI` stands for small integers, `DOUBLE` for double values and the rest is a `PACKED_ELEMENTS`. `HOLEY_{}` elements kindes are a little different. It means the array has holes in it or better said it has empty entries. In this case, the JavaScript engine has to perform more checks and needs to follow the prototype chain up what takes ways longer that checks on a `PACKED_{}` array.

4. HOLEY_SMI_ELEMENTS

```javascript
const array = [1, 2, , 4];
```

5. HOLEY_DOUBLE_ELEMENTS

```javascript
const array = [1, 2, , 4, 5.2];
```

6. HOLEY_ELEMENTS

```javascript
const array = [1, 2, , 4, 'string'];
```

Elements kinds can change. If you change your array on the fly like below, it gets transformed into the next state of elements kinds. You can transform elements kinds from specific to generic but never backwards. So if you have a `HOLEY_ELEMENTS` array, it stays holey forever and takes longer to optimize.

```javascript
const array = [1, 2, 3];
// PACKED_SMI_ELEMENTS

array.push(4.5);
// PACKED_DOUBLE_ELEMENTS
```

Great so now just rewrite every line of code? Nope. Remember the advice from above --> always measure before optimize. Optimizing all your arrays like above can even lead to a slower performance if you do not measure. If you have an array that is not holey and you add many items to it, the V8 has to re-allocate memory. In this case, it can be better to optimize the creation of the arrays instead of the elements kindes and allocate the arrays the known size (`new Array(n)`).

If you want to check what kind of elements your line of code has, you can follow the instructions on a [blog post](https://v8.dev/blog/elements-kinds#debugging) if Mathias Bynens.

#### Takeaways for arrays <div id="takearrays"></div>

- Use consistent indices starting at 0 for Arrays
- Only pre-allocate large Arrays if you are sure they are going to be big. If you have an average array size, grow as you go.
- Don't delete elements in Arrays.
- Don't access deleted elements or indices in Arrays.

## Conclusion <div id="conclusion"></div>

You find [all the resources](https://github.com/marcradziwill/javascript-performance-tutorial) for this article on GitHub. Feel free to check it out, try some stuff or fork it.

In this post, I tried to show you some bits of advice you can use to optimize your JavaScript code for performance issues and some understanding of what happens under the hood of JavaScript engines.

If you like this article, smile for a moment, share it, follow me, check out my RSS feed and [subscribe to my newsletter](https://marcradziwill.com/newsletter/).

Cheers Marc

Photo by [RKTKN](https://unsplash.com/photos/BZ9z9ZeUjbI) on [Unsplash](https://unsplash.com)