How JavaScript Works: Inside the V8 Engine + 5 Tips on How to Write Optimized code

Couple of weeks ago we started a series aimed at digging deeper into JavaScript and how it actually works: we thought that by knowing the building blocks of JavaScript and how they come to play together you’ll be able to write better code and apps.

A few weeks ago, we started a series of articles aimed at digging deeper into JavaScript and how it actually works: We think that by understanding the building blocks of JavaScript and how they work together, you’ll be able to write better code and applications.

The first post of the series focused on providing an overview of the engine, The Runtime and the call stack. This second post will be diving into the internal parts of Google’s V8 JavaScript Engine. We’ll also provide a few quick tips on how to write better JavaScript code — best practices our development team at SessionStack follows when building the product.

The first article in this series focuses on the concepts of engines, runtimes, and call stacks. This second article will dive into the bowels of the Google V8 JavaScript engine. We’ll also offer some tips on how to write better JavaScript code — the best principle our development team has in the SessionStack.

Overview

A JavaScript engine is a program or an interpreter which executes JavaScript code. A JavaScript engine can be implemented as a standard interpreter, or just-in-time compiler that compiles JavaScript to bytecode in some form.

A JavaScript engine is a program or interpreter that executes JavaScript code. A JavaScript engine can be implemented as a standard interpreter or in some form as a just-in-time compiler that compiles JavaScript to bytecode.

This is a list of popular projects that are implementing a JavaScript engine:

Here is a list of hot projects implementing JavaScript engines:

  • V8: open source, developed by Google, written in C++
  • Rhin: Managed by the Mozilla Foundation, it is open source and developed entirely in Java
  • SpiderMonkey: The first JavaScript engine used in Netscape Navigator and now works in Firefox
  • JavaScriptCore: Open source, developed by Nitro and used in Safari by Apple
  • KJS: Originally developed by Harri Porten as the Konqueror Web browser for the KDE project
  • Chakra (JScript9) : Internet Explorer
  • Chakra (JavaScript) : Microsoft Edge
  • Nashorn: Open source, written as part of the OpenJDK by the Oracle Java Language and Tools group
  • JerryScript: A lightweight engine for the Internet of Things

Why build a V8 engine? (Why was the V8 Engine created?)

The V8 Engine which is built by Google is open source and written in C++. This engine is used inside Google Chrome. Unlike the rest of the engines, however, V8 is also used for the popular Node.js runtime.

The V8 engine built by Google is open source and written in C++. The engine is available in Google Chrome. However, unlike other engines V8 is also used in the popular Node.js runtime.

V8 was first designed to increase the performance of JavaScript execution inside web browsers. In order to obtain speed, V8 translates JavaScript code into more efficient machine code instead of using an interpreter. It compiles JavaScript code into machine code at execution by implementing a JIT (Just-In-Time) compiler like a lot of modern JavaScript Engines do such as SpiderMonkey or Rhino (Mozilla). The main difference here is that V8 doesn’t produce bytecode or any intermediate code.

V8 was originally designed to improve the performance of JavaScript execution in Web browsers. For faster execution, V8 converts JavaScript code into more efficient machine code rather than using an interpreter. It compiles JavaScript code into machine code by implementing a JIT (just-in-time) compiler, like many modern JavaScript engines (like SpiderMonkey or Rhino (Mozilla)). The main difference is that V8 does not generate bytecode or any intermediate code.

V8 used to have two compilers

Before version 5.9 of V8 came out (released earlier this year), the engine used two compilers:

  • Full-codegen — a simple and very fast compiler that produced simple and relatively slow machine code.
  • Crankshaft — a more complex (Just-in-time) Optimizing Optimizing Code That produced highly.

Prior to the release of V8 5.9 (released earlier this year), the engine used two compilers:

  • Full-codegen – a simple and very fast compiler that generates simple and relatively slow machine code.
  • Crankshaft – More complex (instant) optimized compilers that can generate highly optimized code.

The V8 Engine also uses several threads internally:

  • The main thread does what you would expect: fetch your code, compile it and then execute it
  • There’s also a separate thread for compiling, so that the main thread can keep executing while the former is optimizing the code
  • A Profiler thread that will tell the runtime on which methods we spend a lot of time so that Crankshaft can optimize them
  • A few threads to handle Garbage Collector sweeps

The V8 engine also uses multiple threads internally:

  • The main thread does what you expect: it takes the code, compiles it, and executes it
  • There is also a separate thread for compilation, so the main thread can continue executing while the former is optimizing code
  • The Profiler thread would tell the runtime the methods we were spending a lot of time on so that the Crankshaft compiler could optimize them
  • Several threads that handle garbage collector scans

When first executing the JavaScript code, V8 leverages full-codegen which directly translates the parsed JavaScript into machine code without any transformation. This allows it to start executing machine code very fast. Note that V8 does not use intermediate bytecode representation this way removing the need for an interpreter.

When executing JavaScript code for the first time, V8 leverages full-CodeGen to convert the parsed JavaScript directly into machine code without any conversion. This allows it to start executing machine code very quickly. Note that V8 does not use intermediate bytecode representation, thereby eliminating the need for an interpreter.

When your code has run for some time, the profiler thread has gathered enough data to tell which method should be optimized.

After your code has been running for a while, the Profiler thread has collected enough data to determine which method should be optimized.

Next, Crankshaft optimizations begin in another thread. It translates the JavaScript abstract syntax tree to a high-level static single-assignment (SSA) representation called Hydrogen and tries to optimize that Hydrogen graph. Most optimizations are done at this level.

Next, the Crankshaft optimizes from another thread. It converts the JavaScript abstract syntax tree into a high-level static singleton assignment (SSA) representation called Hydrogen, and attempts to optimize the Hydrogen diagram. Most optimizations are done at this level.

Inlining

The first optimization is inlining as much code as possible in advance. Inlining is the process of replacing a call site (the line of code where the function is called) with the body of the called function. This simple step allows following optimizations to be more meaningful.

The first optimization is to Inlining as much code as possible ahead of time. Inlining is the process of replacing the body of the called function with the calling location (the line of code on which the function is located). This simple step makes the following optimizations more meaningful.

Hidden Classes

JavaScript is a prototype-based language: there are no classes and objects are created using a cloning process. JavaScript is also a dynamic programming language which means that properties can be easily added or removed from an object after its instantiation.

JavaScript is a prototype-based language: there is no process of creating classes and objects using clones. JavaScript is also a dynamic programming language, which means you can easily add or remove properties from an object after instantiation.

Most JavaScript interpreters use dictionary-like structures (hash function based) to store the location of object property values in the memory. This structure makes retrieving the value of a property in JavaScript more computationally expensive than it would be in a non-dynamic programming language like Java or C#. In Java, all of the object properties are determined by a fixed object layout before compilation and cannot be dynamically added or removed at runtime (well, C# has the dynamic type which is another topic). As a result, the values of properties (or pointers to those properties) can be stored as a continuous buffer in the memory with a fixed-offset between each. The length of an offset can easily be determined based on the property type, whereas this is not possible in JavaScript where a property type can change during runtime.

Most JavaScript interpreters use dictionary-like structures (based on hash functions) to store the locations of object attribute values in memory. This structure makes retrieving the value of an attribute in JavaScript more expensive than it would be in a non-dynamic programming language like Java or C#. In Java, all object properties are determined by the fixed object layout before compilation, and cannot be added or removed dynamically at run time (C# has dynamic typing, which is another topic). Thus, attribute values (or Pointers to these attributes) can be stored in memory as continuous buffers with fixed offsets between them, the length of which can be easily determined based on the attribute type. In JavaScript, property types can change at run time, which is not possible.

Since using dictionaries to find the location of object properties in the memory is very inefficient, V8 uses a different method instead: hidden classes. Hidden classes work similarly to the fixed object layouts (classes) used in languages like Java, except they are created at runtime. Now, let’s see what they actually look like:

Since using a dictionary to find the location of object attributes in memory is inefficient, V8 uses a different approach instead: hiding classes. Hidden classes work similarly to the fixed object layouts (classes) used in the Java language, except that they are created at run time. Now, let’s look at them in action:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);Copy the code

Once the new Point(1,2) invocation happens, V8 will create a hidden class called C0.

Once new Point(1,2) is called, V8 creates a hidden class, C0.

No properties have been defined for Point yet, so C0 is empty.

No attribute is defined for Point, so C0 is null.

Once the first statement this.x = x is executed (inside the Point function), V8 will create a second hidden class called C1 that is based on C0. C1 describes the location in the memory (relative to the object pointer) where the property x can be found. In this case, x is stored at offset 0, which means that when viewing a point object in the memory as a continuous buffer, the first offset will correspond to property x. V8 will also update C0 with a class transition which states that if a property x is added to a point object, the hidden class should switch from C0 to C1. The hidden class for the point object below is now C1.

Once the first statement this.x = x is executed (in the Point function), V8 creates a second hidden class C1 based on C0. C1 describes the location in memory (relative to the object pointer) where attribute X can be found. In this case, x is stored at offset 0, which means that when the point object in memory is viewed as a continuous buffer, the first offset will correspond to attribute X. V8 will also update C0 with class conversion, meaning that if an attribute X is added to a point object, the hidden class should be switched from C0 to C1. The hidden class of the point object below is now C1.

Every time a new property is added to an object, the old hidden class is updated with a transition path to the new hidden class. Hidden class transitions are important because they allow hidden classes to be shared among objects that are created the same way. If two objects share a hidden class and the same property is added to both of them, transitions will ensure that both objects receive the same new hidden class and all the optimized code that comes with it.

Each time a new property is added to the object, the old hidden class is updated to the new hidden class using the transformation path. Hidden class conversions are important because they allow hidden classes to be shared between objects created in the same way. If two objects share a hidden class and the same properties are added to them, the transformation ensures that both objects receive the same new hidden class and all the optimized code that goes with it.

This process is repeated when the statement this.y = y is executed (again, inside the Point function, after the this.x = x statement).

This process is repeated when the statement this.y = y is executed (inside the Point function, after the this.x = x statement).

A new hidden class called C2 is created, a class transition is added to C1 stating that if a property y is added to a Point object (that already contains property x) then the hidden class should change to C2, and the point object’s hidden class is updated to C2.

A new hidden class named C2 is created and the class transformation will be added to C1, indicating that if attribute Y is added to the Point object (which already contains attribute X), the hidden class should be changed to C2 and the hidden class of the Point object updated to C2.

Hidden class transitions are dependent on the order in which properties are added to an object. Take a look at the code snippet below:

The transformation of a hidden class depends on the order in which attributes are added to the object. Take a look at the following code snippet:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;Copy the code

Now, you would assume that for both p1 and p2 the same hidden classes and transitions would be used. Well, not really. For p1, first the property a will be added and then the property b. For p2, however, first b is being assigned, followed by a. Thus, p1 and p2 end up with different hidden classes as a result of the different transition paths. In such cases, It’s much better to initialize dynamic properties in the same order so that the hidden classes can be resused.

Now, you can assume that the same hidden classes and transformations will be used for P1 and P2. It’s not the same. For P1, attribute A is added first, followed by attribute B. But for P2, we allocate B first, and then we allocate A. Thus, P1 and P2 end up with different hidden classes due to different transformation paths. In this case, it is better to initialize the dynamic properties in the same order so that the hidden classes can be reused.

Inline caching

V8 takes advantage of another technique for optimizing dynamically typed languages called inline caching. Inline caching relies on the observation that repeated calls to the same method tend to occur on the same type of object. An in-depth explanation of inline caching can be found here.

V8 leverages another technique called inline caching to optimize dynamically typed languages. Inline caching relies on the observation of repeated calls to the same method that tend to occur on objects of the same type. An in-depth explanation of inline caching can be found here.

We’re going to touch upon the general concept of inline caching (in case you don’t have the time to go through the in-depth explanation above).

We’ll briefly explain the general concept of inline caching (if you don’t have time to go through the in-depth explanation above).

So how does it work? V8 maintains a cache of the type of objects that were passed as a parameter in recent method calls and uses this information to make an assumption about the type of object that will be passed as a parameter in the future. If V8 is able to make a good assumption about the type of object that will be passed to a method, it can bypass the process of figuring out how to access the object’s properties, and instead, use the stored information from previous lookups to the object’s hidden class.

So how does it work? V8 maintains a cache of object types passed as parameters in recent method calls and uses this information to make assumptions about object types passed as parameters in the future. If V8 can make a good assumption about the type of object passed to the method in the future, it can bypass the process of how to access the properties of the object and instead use the information stored from the hidden class of the object previously looked up.

So how are the concepts of hidden classes and inline caching related? Whenever a method is called on a specific object, the V8 engine has to perform a lookup to the hidden class of that object in order to determine the offset for accessing a specific property. After two successful calls of the same method to the same hidden class, V8 omits the hidden class lookup and simply adds the offset of the property to the object pointer itself. For all future calls of that method, the V8 engine assumes that the hidden class hasn’t changed, and jumps directly into the memory address for a specific property using the offsets stored from previous lookups. This greatly increases execution speed.

So how does the concept of hidden classes relate to inline caching? Whenever a method is called on a particular object, the V8 engine must perform a lookup on that object’s hidden class to determine the offset to access a particular property. After two successful calls to the same method on the same hidden class, V8 omits the hidden class lookup and adds the offset of the property to the object pointer itself. For all future calls of this method, the V8 engine assumes that the hidden class has not changed and jumps directly to the memory address of the particular property using the offset stored in the previous lookup. This greatly improves execution speed.

Inline caching is also the reason why it’s so important that objects of the same type share hidden classes. If you create two objects of the same type and with different hidden classes (as we did in the example earlier), V8 won’t be able to use inline caching because even though the two objects are of the same type, their corresponding hidden classes assign different offsets to their properties.

Inline caching is also an important reason why objects of the same type share hidden classes. If you create two objects of the same type and different hidden classes (as in the previous example), V8 will not be able to use inline caching because even if the two objects are of the same type, their corresponding hidden classes assign different offsets to their attributes.

The objects are basically the same, but the ‘A’ and ‘b’ attributes are created in different order

Compilation to machine code

Once the Hydrogen graph is optimized, Crankshaft lowers it to a lower-level representation called Lithium. Most of the Lithium implementation is architecture-specific. Register allocation happens at this level.

Once the Hydrogen diagram was optimized, Crankshaft reduced it to a lower level representation called Lithium. Most implementations of Lithium are architecture oriented. Register allocation occurs at this level.

In the end, Lithium is compiled into machine code. Then something else happens called OSR: on-stack replacement. Before we started compiling and optimizing an obviously long-running method, we were likely running it. V8 is not going to forget what it just slowly executed to start again with the optimized version. Instead, it will transform all the context we have (stack, registers) so that we can switch to the optimized version in the middle of the execution. This is a very complex task, having in mind that among other optimizations, V8 has inlined the code initially. V8 is not the only engine capable of doing it.

Finally, Lithium is compiled into machine code. Then something else happens called OSR: stack replacement. We might run an apparently long-running method before we start compiling and optimizing it. V8 will not forget the result of its slow execution and will not run it again. Instead, it will transform all the contexts (stacks, registers) so that we can switch to the optimized version during execution. This is a very complex task, keep in mind that, among other optimizations, V8 is already inlined at initialization. V8 is not the only engine that can do this.

There are safeguards called deoptimization to make the opposite transformation and revert back to the non-optimized code in case an assumption the engine made doesn’t hold true anymore.

There is a safeguard called de-optimization, where the reverse transformation is made and the code reverts to non-optimized code in case the assumptions the engine made earlier no longer hold (assuming the hidden classes haven’t changed).

Garbage Collection

For garbage collection, V8 uses a traditional generational approach of mark-and-sweep to clean the old generation. The marking phase is supposed to stop the JavaScript execution. In order to control GC costs and make the execution more stable, V8 uses incremental marking: instead of walking the whole heap, trying to mark every possible object, it only walk part of the heap, then resumes normal execution. The next GC stop will continue from where the previous heap walk has stopped. This allows for very short pauses during the normal execution. As mentioned before, the sweep phase is handled by separate threads.

For garbage collection, V8 uses the traditional mark-and-sweep sweep approach to old Generation. The markup phase should stop JavaScript execution. To control GC costs and make execution more stable, V8 uses incremental markup: Instead of traversing the entire heap, trying to mark every possible object, just traversing a portion of the heap, and then resuming normal execution. The next GC picks up where the previous iteration stopped. This allows for very short pauses during normal execution. As mentioned earlier, the scanning phase is handled by a separate thread.

Ignition and TurboFan

With the release of V8 5.9 earlier in 2017, a new execution pipeline was introduced. This new pipeline achieves even bigger performance improvements and significant memory savings in real-world JavaScript applications.

With the release of V8 5.9 earlier in 2017, the new implementation process was rolled out. This new pipeline architecture achieves greater performance improvements and significant memory savings in real-world JavaScript applications.

The new execution pipeline is built on top of Ignition, V8’s interpreter, and TurboFan, V8’s newest optimizing compiler.

The new execution pipeline builds on V8’s new interpreter, On Top of Ignition, and TurboFan, V8’s latest optimized compiler.

You can check out the blog post from the V8 team about the topic here.

You can check out the V8 team blog post on this topic.

Since version 5.9 of V8 came out, full-codegen and Crankshaft (the technologies that have served V8 since 2010) have no longer been used by V8 for JavaScript execution as the V8 team has struggled to keep pace with the new JavaScript language features and the optimizations needed for these features.

With the release of V8 5.9, V8 will no longer use full-CodeGen and Crankshaft (the technology that had served V8 since 2010) as the V8 team struggled to keep up with new JavaScript language features that needed optimization.

This means that overall V8 will have much simpler and more maintainable architecture going forward.

This means that overall V8 will have a simpler and more maintainable architecture.

Benchmarks for Web and Node.js performance improvements

These improvements are just the start. The new Ignition and TurboFan pipeline pave the way for further optimizations that will boost JavaScript performance and shrink V8’s footprint in both Chrome and Node.js in the coming years.

These improvements are just the beginning. The new Ignition and TurboFan pipelines pave the way for further optimizations that will boost JavaScript performance and shrink V8’s footprint in Chrome and Node.js over the next few years.

Finally, here are some tips and tricks on how to write well-optimized, better JavaScript. You can easily derive these from the content above, however, here’s a summary for your convenience:

Finally, here are some tips on how to write better, better JavaScript. You can easily get them from above, but, for convenience, here’s a summary:

How to Write Optimized JavaScript

1.Order of object properties: always instantiate your object properties in the same order so that hidden classes, and subsequently optimized code, can be shared.

Order of object properties: Object properties are always instantiated in the same order so that hidden classes and subsequently optimized code can be shared.

2.Dynamic properties: adding properties to an object after instantiation will force a hidden class change and slow down any methods that were optimized for the previous hidden class. Instead, assign all of an object’s properties in its constructor.

Dynamic properties: Adding properties to an object after instantiation forces class changes to be hidden and slows down any methods optimized for previously hidden classes. Instead, all attributes of the object are allocated in its constructor.

3.Methods: code that executes the same method repeatedly will run faster than code that executes many different methods only once (due to inline caching).

Method: Code that executes the same method repeatedly will run faster than code that executes only once (due to inline caching).

4.Arrays: Avoid Sparse Arrays where keys are not incremental numbers. Sparse Arrays which don’t have every element inside them are a hash table. Elements in such arrays are more expensive to access. Also, Try to avoid pre-allocating large arrays. It’s better to grow as you go. Finally, allocating large arrays. Don’t delete elements in Arrays. It makes the keys sparse.

Arrays: Avoid sparse arrays whose keys are not incremental numbers. A sparse array that does not contain every element is a hash table. Accessing such array elements is more expensive. Also, try to avoid preallocating large arrays. It’s best to grow with your usage. Finally, do not delete elements in the array. It sparkles key values.

5.Tagged values: V8 represents objects and numbers with 32 bits. It uses a bit to know if it is an object (flag = 1) or an integer (flag = 0) called SMI (SMall Integer) because of its 31 bits. Then, if a numeric value is bigger than 31 bits, V8 will box the number, turning it into a

double and creating a new object to put the number inside. Try to use 31 bit signed numbers whenever possible to avoid the expensive boxing operation into a JS object.

Flag values: V8 uses 32 bits to represent objects and numbers. It uses a bit to know whether it is an object (flag = 1) or an Integer called SMI (SMall Integer) (flag = 0), since it is 31 bits. Therefore, if a number is greater than 31 bits, V8 will convert the number to a double and create a new object to put the number in. Use 31-bit signed numbers whenever possible to avoid the expensive boxing operation of converting numbers into JC objects.

We at SessionStack try to follow these best practices in writing highly optimized JavaScript code. The reason is that once you integrate SessionStack into your production web app, it starts recording everything: all DOM changes, user interactions, JavaScript exceptions, stack traces, failed network requests, and debug messages.

We try to follow these best practices in SessionStack to write highly optimized JavaScript code. The reason is that once SessionStack is integrated into a production network application, it starts logging everything: all DOM changes, user interactions, JavaScript exceptions, stack traces, failed network requests, and debug messages.

With SessionStack, you can replay issues in your web apps as videos and see everything that happened to your user. And all of this has to happen with no performance impact for your web app.

With SessionStack, you can reproduce problems in your Web application as videos and see what’s happening to your users. Everything must be redone and not affect the performance of your Web application.

There is a free plan that allows you to get started for free.

Here’s a free program to get you started for free.

resources

If you have any questions, please go to the following address for discussion: How JavaScript works: The Internals of the V8 Engine and 5 Tips for writing optimized code.